weather.scm 21 KB

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