weather.scm 25 KB

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