weather.scm 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2017 Ricardo Wurmus <rekado@elephly.net>
  4. ;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
  5. ;;; Copyright © 2020 Simon Tournier <zimon.toutoune@gmail.com>
  6. ;;;
  7. ;;; This file is part of GNU Guix.
  8. ;;;
  9. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  10. ;;; under the terms of the GNU General Public License as published by
  11. ;;; the Free Software Foundation; either version 3 of the License, or (at
  12. ;;; your option) any later version.
  13. ;;;
  14. ;;; GNU Guix is distributed in the hope that it will be useful, but
  15. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  16. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. ;;; GNU General Public License for more details.
  18. ;;;
  19. ;;; You should have received a copy of the GNU General Public License
  20. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  21. (define-module (guix scripts weather)
  22. #:use-module (guix ui)
  23. #:use-module (guix scripts)
  24. #:use-module (guix packages)
  25. #:use-module (guix profiles)
  26. #:use-module (guix derivations)
  27. #:use-module (guix progress)
  28. #:use-module (guix monads)
  29. #:use-module (guix store)
  30. #:use-module (guix grafts)
  31. #:use-module (guix gexp)
  32. #:use-module ((guix build syscalls) #:select (terminal-columns))
  33. #:use-module ((guix build utils) #:select (every*))
  34. #:use-module (guix substitutes)
  35. #:use-module (guix narinfo)
  36. #:use-module (guix http-client)
  37. #:use-module (guix ci)
  38. #:use-module (guix sets)
  39. #:use-module (guix graph)
  40. #:autoload (guix scripts graph) (%bag-node-type)
  41. #:use-module (gnu packages)
  42. #:use-module (web uri)
  43. #:use-module (srfi srfi-1)
  44. #:use-module (srfi srfi-19)
  45. #:use-module (srfi srfi-26)
  46. #:use-module (srfi srfi-34)
  47. #:use-module (srfi srfi-37)
  48. #:use-module (ice-9 match)
  49. #:use-module (ice-9 format)
  50. #:use-module (ice-9 vlist)
  51. #:export (guix-weather))
  52. (define (all-packages)
  53. "Return the list of public packages we are going to query."
  54. (fold-packages (lambda (package result)
  55. (match (package-replacement package)
  56. ((? package? replacement)
  57. (cons* replacement package result))
  58. (#f
  59. (cons package result))))
  60. '()
  61. ;; Dismiss deprecated packages but keep hidden packages.
  62. #:select? (negate package-superseded)))
  63. (define (call-with-progress-reporter reporter proc)
  64. "This is a variant of 'call-with-progress-reporter' that works with monadic
  65. scope."
  66. ;; TODO: Move to a more appropriate place.
  67. (with-monad %store-monad
  68. (start-progress-reporter! reporter)
  69. (mlet* %store-monad ((report -> (lambda ()
  70. (progress-reporter-report! reporter)))
  71. (result (proc report)))
  72. (stop-progress-reporter! reporter)
  73. (return result))))
  74. (define* (package-outputs packages
  75. #:optional (system (%current-system)))
  76. "Return the list of outputs of all of PACKAGES for the given SYSTEM."
  77. (define (lower-object/no-grafts obj system)
  78. (mlet* %store-monad ((previous (set-grafting #f))
  79. (drv (lower-object obj system))
  80. (_ (set-grafting previous)))
  81. (return drv)))
  82. (let ((packages (filter (lambda (package)
  83. (or (not (package? package))
  84. (supported-package? package system)))
  85. packages)))
  86. (format (current-error-port)
  87. (G_ "computing ~h package derivations for ~a...~%")
  88. (length packages) system)
  89. (call-with-progress-reporter (progress-reporter/bar (length packages))
  90. (lambda (report)
  91. (foldm %store-monad
  92. (lambda (package result)
  93. ;; PACKAGE could in fact be a non-package object, for example
  94. ;; coming from a user-specified manifest. Thus, use
  95. ;; 'lower-object' rather than 'package->derivation' here.
  96. (mlet %store-monad ((drv (lower-object/no-grafts package
  97. system)))
  98. (report)
  99. (match (derivation->output-paths drv)
  100. (((names . items) ...)
  101. (return (append items result))))))
  102. '()
  103. packages)))))
  104. (define (call-with-time thunk kont)
  105. "Call THUNK and pass KONT the elapsed time followed by THUNK's return
  106. values."
  107. (let* ((start (current-time time-monotonic))
  108. (result (call-with-values thunk list))
  109. (end (current-time time-monotonic)))
  110. (apply kont (time-difference end start) result)))
  111. (define-syntax-rule (let/time ((time result ... exp)) body ...)
  112. (call-with-time (lambda () exp) (lambda (time result ...) body ...)))
  113. (define (histogram field proc seed lst)
  114. "Return an alist giving a histogram of all the values of FIELD for elements
  115. of LST. FIELD must be a one element procedure that returns a field's value.
  116. For each FIELD value, call PROC with the previous field-specific result.
  117. Example:
  118. (histogram car (lambda (x n) (+ 1 n)) 0 '((a . x)(b . y)(a . z)))
  119. => ((a . 2) (b . 1))
  120. meaning that we have two a's and one b."
  121. (let loop ((lst lst)
  122. (result '()))
  123. (match lst
  124. (()
  125. result)
  126. ((head . tail)
  127. (let ((value (field head)))
  128. (loop tail
  129. (match (assoc-ref result value)
  130. (#f
  131. `((,value . ,(proc head seed)) ,@result))
  132. (previous
  133. `((,value . ,(proc head previous))
  134. ,@(alist-delete value result))))))))))
  135. (define (throughput lst timestamp)
  136. "Return the throughput, in items per second, given the elements of LST,
  137. calling TIMESTAMP to get the \"timestamp\" of each item."
  138. (let ((oldest (reduce min +inf.0 (map build-timestamp lst)))
  139. (now (time-second (current-time time-utc))))
  140. (/ (length lst) (- now oldest) 1.)))
  141. (define (queued-subset queue items)
  142. "Return the subset of ITEMS, a list of store file names, that appears in
  143. QUEUE, a list of builds. Return #f if elements in QUEUE lack information
  144. about the derivations queued, as is the case with Hydra."
  145. (define queued
  146. (append-map (lambda (build)
  147. (match (false-if-exception
  148. (read-derivation-from-file (build-derivation build)))
  149. (#f
  150. '())
  151. (drv
  152. (match (derivation->output-paths drv)
  153. (((names . items) ...) items)))))
  154. queue))
  155. (if (any (negate build-derivation) queue)
  156. #f ;no derivation information
  157. (lset-intersection string=? queued items)))
  158. (define* (report-server-coverage server items
  159. #:key display-missing?)
  160. "Report the subset of ITEMS available as substitutes on SERVER.
  161. When DISPLAY-MISSING? is true, display the list of missing substitutes.
  162. Return the coverage ratio, an exact number between 0 and 1."
  163. (define MiB (* (expt 2 20) 1.))
  164. (format #t (G_ "looking for ~h store items on ~a...~%")
  165. (length items) server)
  166. (let/time ((time narinfos requests-made
  167. (lookup-narinfos
  168. server items
  169. #:make-progress-reporter
  170. (lambda* (total #:key url #:allow-other-keys)
  171. (progress-reporter/bar total)))))
  172. (format #t "~a~%" server)
  173. (let ((obtained (length narinfos))
  174. (requested (length items))
  175. (missing (lset-difference string=?
  176. items (map narinfo-path narinfos)))
  177. (sizes (append-map (lambda (narinfo)
  178. (filter integer?
  179. (narinfo-file-sizes narinfo)))
  180. narinfos))
  181. (time (+ (time-second time)
  182. (/ (time-nanosecond time) 1e9))))
  183. (format #t (G_ " ~,1f% substitutes available (~h out of ~h)~%")
  184. (* 100. (/ obtained requested 1.))
  185. obtained requested)
  186. (let ((total (/ (reduce + 0 sizes) MiB)))
  187. (match (length sizes)
  188. ((? zero?)
  189. (format #t (G_ " unknown substitute sizes~%")))
  190. (len
  191. (if (= len obtained)
  192. (format #t (G_ " ~,1h MiB of nars (compressed)~%") total)
  193. (format #t (G_ " at least ~,1h MiB of nars (compressed)~%")
  194. total)))))
  195. (format #t (G_ " ~,1h MiB on disk (uncompressed)~%")
  196. (/ (reduce + 0 (map narinfo-size narinfos)) MiB))
  197. (when (> requests-made 0)
  198. (format #t (G_ " ~,3h seconds per request (~,1h seconds in total)~%")
  199. (/ time requests-made 1.) time)
  200. (format #t (G_ " ~,1h requests per second~%")
  201. (/ requests-made time 1.)))
  202. (guard (c ((http-get-error? c)
  203. (if (= 404 (http-get-error-code c))
  204. (format (current-error-port)
  205. (G_ " (continuous integration information \
  206. unavailable)~%"))
  207. (format (current-error-port)
  208. (G_ " '~a' returned ~a (~s)~%")
  209. (uri->string (http-get-error-uri c))
  210. (http-get-error-code c)
  211. (http-get-error-reason c)))))
  212. (let* ((max %query-limit)
  213. (queue (queued-builds server max))
  214. (len (length queue))
  215. (histo (histogram build-system
  216. (lambda (build count)
  217. (+ 1 count))
  218. 0 queue)))
  219. (newline)
  220. (unless (null? missing)
  221. (match (queued-subset queue missing)
  222. (#f #f)
  223. ((= length queued)
  224. (let ((missing (length missing)))
  225. (format #t (G_ " ~,1f% (~h out of ~h) of the missing items \
  226. are queued~%")
  227. (* 100. (/ queued missing))
  228. queued missing)))))
  229. (if (>= len max)
  230. (format #t (G_ " at least ~h queued builds~%") len)
  231. (format #t (G_ " ~h queued builds~%") len))
  232. (for-each (match-lambda
  233. ((system . count)
  234. (format #t (G_ " ~a: ~a (~0,1f%)~%")
  235. system count (* 100. (/ count len)))))
  236. histo))
  237. (let* ((latest (latest-builds server))
  238. (builds/sec (throughput latest build-timestamp)))
  239. (format #t (G_ " build rate: ~1,2f builds per hour~%")
  240. (* builds/sec 3600.))
  241. (for-each (match-lambda
  242. ((system . builds)
  243. (format #t (G_ " ~a: ~,2f builds per hour~%")
  244. system
  245. (* (throughput builds build-timestamp)
  246. 3600.))))
  247. (histogram build-system cons '() latest))))
  248. (when (and display-missing? (not (null? missing)))
  249. (newline)
  250. (format #t (G_ "Substitutes are missing for the following items:~%"))
  251. (format #t "~{ ~a~%~}" missing))
  252. ;; Return the coverage ratio.
  253. (let ((total (length items)))
  254. (/ (- total (length missing)) total)))))
  255. ;;;
  256. ;;; Command-line options.
  257. ;;;
  258. (define (show-help)
  259. (display (G_ "Usage: guix weather [OPTIONS] [PACKAGES ...]
  260. Report the availability of substitutes.\n"))
  261. (display (G_ "
  262. --substitute-urls=URLS
  263. check for available substitutes at URLS"))
  264. (display (G_ "
  265. -m, --manifest=MANIFEST
  266. look up substitutes for packages specified in MANIFEST"))
  267. (display (G_ "
  268. -c, --coverage[=COUNT]
  269. show substitute coverage for packages with at least
  270. COUNT dependents"))
  271. (display (G_ "
  272. --display-missing display the list of missing substitutes"))
  273. (display (G_ "
  274. -s, --system=SYSTEM consider substitutes for SYSTEM--e.g., \"i686-linux\""))
  275. (newline)
  276. (display (G_ "
  277. -h, --help display this help and exit"))
  278. (display (G_ "
  279. -V, --version display version information and exit"))
  280. (newline)
  281. (show-bug-report-information))
  282. (define %options
  283. (list (option '(#\h "help") #f #f
  284. (lambda args
  285. (show-help)
  286. (exit 0)))
  287. (option '(#\V "version") #f #f
  288. (lambda args
  289. (show-version-and-exit "guix weather")))
  290. (option '("substitute-urls") #t #f
  291. (lambda (opt name arg result . rest)
  292. (let ((urls (string-tokenize arg)))
  293. (for-each (lambda (url)
  294. (unless (string->uri url)
  295. (leave (G_ "~a: invalid URL~%") url)))
  296. urls)
  297. (apply values
  298. (alist-cons 'substitute-urls urls
  299. (alist-delete 'substitute-urls result))
  300. rest))))
  301. (option '(#\m "manifest") #t #f
  302. (lambda (opt name arg result)
  303. (alist-cons 'manifest arg result)))
  304. (option '(#\c "coverage") #f #t
  305. (lambda (opt name arg result)
  306. (alist-cons 'coverage
  307. (if arg (string->number* arg) 0)
  308. result)))
  309. (option '("display-missing") #f #f
  310. (lambda (opt name arg result)
  311. (alist-cons 'display-missing? #t result)))
  312. (option '(#\s "system") #t #f
  313. (lambda (opt name arg result)
  314. (alist-cons 'system arg result)))))
  315. (define %default-options
  316. `((substitute-urls . ,%default-substitute-urls)))
  317. (define (load-manifest file)
  318. "Load the manifest from FILE and return the list of packages it refers to."
  319. (let* ((user-module (make-user-module '((guix profiles) (gnu))))
  320. (manifest (load* file user-module)))
  321. (delete-duplicates (map manifest-entry-item
  322. (manifest-transitive-entries manifest))
  323. eq?)))
  324. ;;;
  325. ;;; Missing package substitutes.
  326. ;;;
  327. (define* (package-partition-boundary pred packages
  328. #:key (system (%current-system)))
  329. "Return the subset of PACKAGES that are at the \"boundary\" between those
  330. that match PRED and those that don't. The returned packages themselves do not
  331. match PRED but they have at least one direct dependency that does.
  332. Note: The assumption is that, if P matches PRED, then all the dependencies of
  333. P match PRED as well."
  334. ;; XXX: Graph theoreticians surely have something to teach us about this...
  335. (let loop ((packages packages)
  336. (result (setq))
  337. (visited vlist-null))
  338. (define (visited? package)
  339. (vhash-assq package visited))
  340. (match packages
  341. ((package . rest)
  342. (cond ((visited? package)
  343. (loop rest result visited))
  344. ((pred package)
  345. (loop rest result (vhash-consq package #t visited)))
  346. (else
  347. (let* ((bag (package->bag package system))
  348. (deps (filter-map (match-lambda
  349. ((label (? package? package) . _)
  350. (and (not (pred package))
  351. package))
  352. (_ #f))
  353. (bag-direct-inputs bag))))
  354. (loop (append deps rest)
  355. (if (null? deps)
  356. (set-insert package result)
  357. result)
  358. (vhash-consq package #t visited))))))
  359. (()
  360. (set->list result)))))
  361. (define (package->output-mapping packages system)
  362. "Return a vhash that maps each item of PACKAGES to its corresponding output
  363. store file names for SYSTEM."
  364. (foldm %store-monad
  365. (lambda (package mapping)
  366. (mlet %store-monad ((drv (package->derivation package system
  367. #:graft? #f)))
  368. (return (vhash-consq package
  369. (match (derivation->output-paths drv)
  370. (((names . outputs) ...)
  371. outputs))
  372. mapping))))
  373. vlist-null
  374. packages))
  375. (define (substitute-oracle server items)
  376. "Return a procedure that, when passed a store item (one of those listed in
  377. ITEMS), returns true if SERVER has a substitute for it, false otherwise."
  378. (define available
  379. (fold (lambda (narinfo set)
  380. (set-insert (narinfo-path narinfo) set))
  381. (set)
  382. (lookup-narinfos server items)))
  383. (cut set-contains? available <>))
  384. (define* (report-package-coverage-per-system server packages system
  385. #:key (threshold 0))
  386. "Report on the subset of PACKAGES that lacks SYSTEM substitutes on SERVER,
  387. sorted by decreasing number of dependents. Do not display those with less
  388. than THRESHOLD dependents."
  389. (mlet* %store-monad ((packages -> (package-closure packages #:system system))
  390. (mapping (package->output-mapping packages system))
  391. (back-edges (node-back-edges %bag-node-type packages)))
  392. (define items
  393. (vhash-fold (lambda (package items result)
  394. (append items result))
  395. '()
  396. mapping))
  397. (define substitutable?
  398. (substitute-oracle server items))
  399. (define substitutable-package?
  400. (lambda (package)
  401. (match (vhash-assq package mapping)
  402. ((_ . items)
  403. (find substitutable? items))
  404. (#f
  405. #f))))
  406. (define missing
  407. (package-partition-boundary substitutable-package? packages
  408. #:system system))
  409. (define missing-count
  410. (length missing))
  411. (if (zero? threshold)
  412. (format #t (N_ "The following ~a package is missing from '~a' for \
  413. '~a':~%"
  414. "The following ~a packages are missing from '~a' for \
  415. '~a':~%"
  416. missing-count)
  417. missing-count server system)
  418. (format #t (N_ "~a package is missing from '~a' for '~a':~%"
  419. "~a packages are missing from '~a' for '~a', among \
  420. which:~%"
  421. missing-count)
  422. missing-count server system))
  423. (for-each (match-lambda
  424. ((package count)
  425. (match (vhash-assq package mapping)
  426. ((_ . items)
  427. (when (>= count threshold)
  428. (format #t " ~4d\t~a@~a\t~{~a ~}~%"
  429. count
  430. (package-name package) (package-version package)
  431. items)))
  432. (#f ;PACKAGE must be an internal thing
  433. #f))))
  434. (sort (zip missing
  435. (map (lambda (package)
  436. (node-reachable-count (list package)
  437. back-edges))
  438. missing))
  439. (match-lambda*
  440. (((_ count1) (_ count2))
  441. (< count2 count1)))))
  442. (return #t)))
  443. (define* (report-package-coverage server packages systems
  444. #:key (threshold 0))
  445. "Report on the substitute coverage for PACKAGES, for each of SYSTEMS, on
  446. SERVER. Display information for packages with at least THRESHOLD dependents."
  447. (with-store store
  448. (run-with-store store
  449. (foldm %store-monad
  450. (lambda (system _)
  451. (report-package-coverage-per-system server packages system
  452. #:threshold threshold))
  453. #f
  454. systems))))
  455. ;;;
  456. ;;; Entry point.
  457. ;;;
  458. (define-command (guix-weather . args)
  459. (synopsis "report on the availability of pre-built package binaries")
  460. (define (package-list opts)
  461. ;; Return the package list specified by OPTS.
  462. (let ((files (filter-map (match-lambda
  463. (('manifest . file) file)
  464. (_ #f))
  465. opts))
  466. (base (filter-map (match-lambda
  467. (('argument . spec)
  468. (specification->package spec))
  469. (_
  470. #f))
  471. opts)))
  472. (if (and (null? files) (null? base))
  473. (all-packages)
  474. (append base (append-map load-manifest files)))))
  475. (with-error-handling
  476. (parameterize ((current-terminal-columns (terminal-columns))
  477. ;; Set grafting upfront in case the user's input depends on
  478. ;; it (e.g., a manifest or code snippet that calls
  479. ;; 'gexp->derivation').
  480. (%graft? #f))
  481. (let* ((opts (parse-command-line args %options
  482. (list %default-options)
  483. #:build-options? #f))
  484. (urls (assoc-ref opts 'substitute-urls))
  485. (systems (match (filter-map (match-lambda
  486. (('system . system) system)
  487. (_ #f))
  488. opts)
  489. (() (list (%current-system)))
  490. (systems systems)))
  491. (packages (package-list opts))
  492. (items (with-store store
  493. (concatenate
  494. (run-with-store store
  495. (mapm %store-monad
  496. (lambda (system)
  497. (package-outputs packages system))
  498. systems))))))
  499. (exit
  500. (every* (lambda (server)
  501. (define coverage
  502. (report-server-coverage server items
  503. #:display-missing?
  504. (assoc-ref opts 'display-missing?)))
  505. (match (assoc-ref opts 'coverage)
  506. (#f #f)
  507. (threshold
  508. ;; PACKAGES may include non-package objects coming from a
  509. ;; manifest. Filter them out.
  510. (report-package-coverage server
  511. (filter package? packages)
  512. systems
  513. #:threshold threshold)))
  514. (= 1 coverage))
  515. urls))))))
  516. ;;; Local Variables:
  517. ;;; eval: (put 'let/time 'scheme-indent-function 1)
  518. ;;; End: