1 (* rpmdepsize - visualize the size of RPM dependencies
2 * (C) Copyright 2009 Red Hat Inc.
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 * Written by Richard W.M. Jones <rjones@redhat.com>
19 * Python script modified from a version by Seth Vidal.
31 (* This corresponds to the sexpr that we write out from the
32 * Python code. OCaml will type-check it.
34 type root_packages = string * packages
35 and packages = pkg list
37 nevra : string; (* name-[epoch:]version-release.arch *)
43 size : int64; (* installed size, excl. dirs *)
48 (* Full dependency representation. This is actually a graph because
49 * it contains dependency loops. 'deps list' is a ref because we
50 * update it as we are building it.
52 type deps = Deps of pkg * deps list ref
54 (* Final tree representation, loops removed, and everything we want to
55 * display stored in the nodes.
57 type tree = Tree of pkg * int64 * int64 * GDraw.color * tree list
59 module StringMap = Map.Make (String)
61 let sum = List.fold_left (+^) 0L
62 let spaces n = String.make n ' '
64 (* Python has privileged access to the yum repodata, so we have to use
65 * this Python snippet to pull the data that we need out. This is the
66 * part of the program that takes ages to run, because Python is as
67 * slow as a fat snake that's just eaten a huge lunch. We can't help that.
76 basepkg = yb.pkgSack.returnPackages (patterns=[sys.argv[1]])[0]
77 deps = dict ({basepkg:False})
79 # Recursively find all the dependencies.
83 for pkg in deps.keys():
84 if deps[pkg] == False:
87 for r in pkg.requires:
88 ps = yb.whatProvides (r[0], r[1], r[2])
89 best = yb._bestPackageFromList (ps.returnPackages ())
90 if best.name != pkg.name:
91 deps[pkg].append (best)
92 if not deps.has_key (best):
94 deps[pkg] = yum.misc.unique (deps[pkg])
96 # Get the data out of python as fast as possible so we can
97 # use a serious language for analysis of the tree.
98 print \"(%s (\" % basepkg
99 for pkg in deps.keys():
100 print \"((nevra %s) (name %s) (epoch %s) (version %s) (release %s) (arch %s) (size %s)\" % (pkg, pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch, pkg.installedsize)
105 sys.stdout.write (\"))\") # suppress trailing newline"
108 printf "getting repository information (this can take a few seconds ...)\n%!";
110 (* Run the Python program and read in the generated sexpr. *)
112 sprintf "python -c %s %s"
113 (Filename.quote repoquery_py) (Filename.quote Sys.argv.(1)) in
114 let chan = open_process_in cmd in
115 ignore (input_line chan); (* drop "Loaded plugins" *)
117 root_packages_of_sexp (Sexp.of_string (Std.input_all chan)) in
118 (match close_process_in chan with
120 | WEXITED i -> failwith (sprintf "command exited with status %d" i)
121 | WSIGNALED i | WSTOPPED i ->
122 failwith (sprintf "command stopped with signal %d" i)
127 fun pkg -> printf "%s -> [%s]\n" pkg.nevra (String.concat ", " pkg.deps)
129 printf "root package is %s\n" root;
133 (* Create the dependency graph, probably contains loops so beware. *)
134 let deps = List.map (fun pkg -> Deps (pkg, ref [])) pkgs in
137 fun map (Deps (pkg, _) as deps) ->
138 StringMap.add pkg.nevra deps map
139 ) StringMap.empty deps in
141 fun (Deps (pkg, deps)) ->
142 let deps' = List.map (fun n -> StringMap.find n depsmap) pkg.deps in
143 deps := List.append !deps deps'
145 let deps = () in ignore deps;
148 let seen = ref StringMap.empty in
149 let rec display ?(indent=0) = function
150 | Deps (pkg, deps) ->
151 if StringMap.mem pkg.nevra !seen then
152 printf "%s%s -> ...\n" (spaces indent) pkg.nevra
154 printf "%s%s -> [%s]\n"
155 (spaces indent) pkg.nevra (
157 (List.map (fun (Deps (pkg, _)) -> pkg.nevra) !deps)
159 seen := StringMap.add pkg.nevra true !seen;
160 List.iter (display ~indent:(indent+2)) !deps
163 display (StringMap.find root depsmap);
167 (* For each package, calculate the total installed size of the package,
168 * which includes all subpackages pulled in. So it's what would be
169 * installed if you did 'yum install foo'.
172 let seen = ref StringMap.empty in
173 let rec _total = function
174 | Deps (pkg, _) when StringMap.mem pkg.nevra !seen -> 0L
175 | Deps (pkg, { contents = children }) ->
176 seen := StringMap.add pkg.nevra true !seen;
177 pkg.size +^ sum (List.map _total children)
179 _total (StringMap.find pkg.nevra depsmap)
183 fun map pkg -> StringMap.add pkg.nevra (total pkg) map
184 ) StringMap.empty pkgs in
186 (* Create the final display tree. Each node is sorted so that
187 * children with the largest contribution come first (on the left).
188 * We remove packages which are already installed by earlier
189 * (leftward) packages. At each node we also store total size and
190 * size of the additional packages.
193 let seen = StringMap.empty in
194 let seen = StringMap.add root true seen in
195 let seen = ref seen in
196 let mark_seen (Deps (pkg, _))= seen := StringMap.add pkg.nevra true !seen in
197 let not_seen (Deps (pkg, _)) = not (StringMap.mem pkg.nevra !seen) in
198 let rec build_tree = function
199 | Deps (pkg, { contents = children }) ->
200 (* Sort children by reverse total size. *)
201 let cmp (Deps (p1, _)) (Deps (p2, _)) =
202 let t1 = StringMap.find p1.nevra totalsmap in
203 let t2 = StringMap.find p2.nevra totalsmap in
206 let children = List.sort ~cmp children in
207 let children = List.filter not_seen children in
208 List.iter mark_seen children;
209 let children = List.map build_tree children in
210 let total = StringMap.find pkg.nevra totalsmap in
212 let rec sum_child_sizes = function
213 | Tree (pkg, _, _, _, children) ->
215 fun size child -> size +^ sum_child_sizes child
218 sum_child_sizes (Tree (pkg, 0L, 0L, `WHITE, children)) in
219 Tree (pkg, total, increm, `WHITE, children)
221 build_tree (StringMap.find root depsmap) in
224 let rec display ?(indent=0) = function
225 | Tree (pkg, total, increm, _, children) ->
226 printf "%s%s %Ld/%Ld/%Ld\n%!"
227 (spaces indent) pkg.nevra pkg.size increm total;
228 List.iter (display ~indent:(indent+2)) children
233 (* Max depth of the tree. *)
235 let rec depth = function
236 | Tree (pkg, _, _, _, children) ->
237 List.fold_left (fun d c -> max d (1 + depth c)) 1 children
241 (* Allocate a colour to each node in the tree based on its parent. The
242 * single top node is always light grey. The second level nodes are
246 let Tree (pkg, total, increm, _, level2) = tree in
252 `RGB (55000, 55000, 0);
253 `RGB (0, 55000, 55000);
255 let rec colour_level2 cols = function
257 | Tree (pkg, total, increm, _, level3) :: level2 ->
258 let col, cols = match cols with
259 | [] -> List.hd pcols, List.tl pcols
260 | col :: cols -> col, cols in
261 let level3 = colour_level3 col (List.length level3) 0 level3 in
262 Tree (pkg, total, increm, col, level3)
263 :: colour_level2 cols level2
264 and colour_level3 col n i = function
266 | Tree (pkg, total, increm, _, leveln) :: level3 ->
267 let col = scale_colour col n i in
268 let leveln = colour_level3 col (List.length leveln) 0 leveln in
269 Tree (pkg, total, increm, col, leveln)
270 :: colour_level3 col n (i+1) level3
271 and scale_colour col n i =
272 let r, g, b = match col with
273 | `RGB (r, g, b) -> float r, float g, float b
274 | _ -> assert false in
275 let i = float i and n = float n in
276 let scale = 0.8 +. i/.(5.*.n) in
277 let r = r *. scale in
278 let g = g *. scale in
279 let b = b *. scale in
280 `RGB (int_of_float r, int_of_float g, int_of_float b)
282 colour_level2 pcols level2 in
283 Tree (pkg, total, increm, `RGB (55000, 55000, 55000), level2) in
285 (* top_total is the total size in bytes of everything. Used for
286 * relative display of percentages, widths, etc.
288 let Tree (_, top_total, top_increm, _, _) = tree in
289 assert (top_total = top_increm);
291 (* Useful display functions. *)
292 let display_percent bytes =
293 100. *. Int64.to_float bytes /. Int64.to_float top_total
295 and display_size bytes =
296 if bytes > 104_857L then
297 sprintf "%.1f MB" (Int64.to_float bytes /. 1_048_576.)
298 else if bytes > 102L then
299 sprintf "%.1f KB" (Int64.to_float bytes /. 1_024.)
304 (* Open the window. *)
305 let title = root ^ " - Fedora RPM dependency size viewer" in
307 GWindow.window ~width:800 ~height:600 ~title ~allow_shrink:true () in
309 ignore (window#connect#destroy ~callback:GMain.quit);
311 let da = GMisc.drawing_area ~packing:window#add () in
313 let draw = new GDraw.drawable da#misc#window in
315 (* Need to enable these mouse events so we can do tooltips. *)
316 GtkBase.Widget.add_events da#as_widget
317 [`ENTER_NOTIFY; `LEAVE_NOTIFY; `POINTER_MOTION];
319 let tooltips = ref None in
321 (* To track tooltips, the 'repaint' function records the location of
322 * each box (ie. package) in the drawing area in this private data
323 * structure, and the 'motion' function looks them up in order to
324 * display the right tooltip over each box.
326 let add_locn, reset_locns, get_locn =
327 let rows = ref [||] in
328 let rowheight = ref 0. in
330 let reset_locns rowheight' depth =
331 (* This data structure sucks because we just do a linear search
332 * over each row when looking up the 'x'. Should use some sort
333 * of self-balancing tree instead. XXX
335 rows := Array.init depth (fun _ -> ref []);
336 rowheight := rowheight'
337 and add_locn x yi width thing =
338 let row = (!rows).(yi) in
339 row := ((x, x +. width), thing) :: !row
341 let yi = int_of_float (y /. !rowheight) in
342 if yi >= 0 && yi < Array.length !rows then (
343 let row = !((!rows).(yi)) in
345 (snd (List.find (fun ((xlow, xhi), thing) ->
346 xlow <= x && x < xhi)
348 with Not_found -> None
352 add_locn, reset_locns, get_locn
355 (* Pango contexts used to draw large and small text. *)
356 let pango_large_context = da#misc#create_pango_context in
357 pango_large_context#set_font_description (Pango.Font.from_string "Sans 12");
358 let pango_small_context = da#misc#create_pango_context in
359 pango_small_context#set_font_description (Pango.Font.from_string "Sans 8");
362 (* Get the canvas size and fill the background with white. *)
363 let width, height = draw#size in
364 draw#set_background `WHITE;
365 draw#set_foreground `WHITE;
366 draw#rectangle ~x:0 ~y:0 ~width ~height ~filled:true ();
368 (* Calculate the scales so we can fit everything into the window. *)
369 let rowheight = float height /. float depth in
370 let scale = float width /. Int64.to_float top_total in
372 reset_locns rowheight depth;
374 (* Now draw the tree. *)
375 let rec draw_tree x yi = function
376 | Tree (pkg, total, increm, colour, children) ->
377 (* Draw pkg at (x, y). *)
378 let y = float yi *. rowheight in
379 let width = scale *. Int64.to_float increm in
380 let pkgsizewidth = scale *. Int64.to_float pkg.size in
381 draw_pkg x yi y width pkgsizewidth rowheight colour pkg total increm;
383 (* Draw the children of pkg at (i, y + rowheight), where
384 * i starts as x and increments for each child.
387 let rec loop x = function
389 | child :: children ->
390 draw_tree x yi child;
391 let Tree (_, _, increm, _, _) = child in
392 let childwidth = scale *. Int64.to_float increm in
393 loop (x +. childwidth) children
397 (* Draw a single package. *)
398 and draw_pkg x yi y width pkgsizewidth height colour pkg total increm =
399 add_locn x yi width (colour, pkg, total, increm);
401 let x = int_of_float x in
402 let y = int_of_float y in
403 let width = int_of_float width in
404 let pkgsizewidth = int_of_float pkgsizewidth in
405 let height = int_of_float height in
408 draw_pkg_outline x y width pkgsizewidth height colour;
409 draw_pkg_label x y width height colour pkg total increm
411 else if width >= 4 then
412 draw_pkg_narrow x y width height colour
414 XXX This doesn't work. We need to coalesce small packages
416 draw_pkg_narrow x y 1 height colour *)
418 and draw_pkg_outline x y width pkgsizewidth height colour =
419 draw#set_foreground colour;
420 draw#rectangle ~x:(x+2) ~y:(y+2)
421 ~width:(width-4) ~height:(height-4)
423 if pkgsizewidth > 2 then (
424 draw#set_foreground (darken colour);
425 draw#rectangle ~x:(x+2) ~y:(y+2)
426 ~width:(pkgsizewidth-2) ~height:(height-4)
428 draw#set_foreground (choose_contrasting_colour colour);
429 draw#set_line_attributes ~style:`ON_OFF_DASH ();
430 draw#line (x+pkgsizewidth) (y+2) (x+pkgsizewidth) (y+height-2);
431 draw#set_line_attributes ~style:`SOLID ()
433 draw#set_foreground (`BLACK);
434 draw#rectangle ~x:(x+2) ~y:(y+2)
435 ~width:(width-4) ~height:(height-4)
438 and draw_pkg_label x y width height colour pkg total increm =
439 (* How to write text in a drawing area, in case it's not
440 * obvious, which it certainly is not:
441 * http://www.math.nagoya-u.ac.jp/~garrigue/soft/olabl/lablgtk-list/120.txt
443 (* txt1 is the same as the tooltip. *)
445 let txt = pango_large_context#create_layout in
446 Pango.Layout.set_text txt (
448 Package: %.1f%% %s (%Ld bytes)
449 Incremental: %.1f%% %s (%Ld bytes)
450 Total: %.1f%% %s (%Ld bytes)" pkg.nevra
451 (display_percent pkg.size) (display_size pkg.size) pkg.size
452 (display_percent increm) (display_size increm) increm
453 (display_percent total) (display_size total) total
458 let txt = pango_small_context#create_layout in
459 Pango.Layout.set_text txt (
461 Package: %.1f%% %s (%Ld bytes)
462 Incremental: %.1f%% %s (%Ld bytes)
463 Total: %.1f%% %s (%Ld bytes)" pkg.nevra
464 (display_percent pkg.size) (display_size pkg.size) pkg.size
465 (display_percent increm) (display_size increm) increm
466 (display_percent total) (display_size total) total
471 let txt = pango_small_context#create_layout in
472 Pango.Layout.set_text txt (
474 Pkg: %.1f%% %s (%Ld bytes)
475 Incr: %.1f%% %s (%Ld bytes)
476 Tot: %.1f%% %s (%Ld bytes)" pkg.name
477 (display_percent pkg.size) (display_size pkg.size) pkg.size
478 (display_percent increm) (display_size increm) increm
479 (display_percent total) (display_size total) total
484 let txt = pango_small_context#create_layout in
485 Pango.Layout.set_text txt (
489 Tot: %.1f%% %s" pkg.name
490 (display_percent pkg.size) (display_size pkg.size)
491 (display_percent increm) (display_size increm)
492 (display_percent total) (display_size total)
497 let txt = pango_small_context#create_layout in
498 Pango.Layout.set_text txt (
499 sprintf "%s\nPkg: %.1f%%\nIncr: %.1f%%\nTot: %.1f%%"
501 (display_percent pkg.size)
502 (display_percent increm)
503 (display_percent total)
508 let txt = pango_small_context#create_layout in
509 Pango.Layout.set_text txt (
510 sprintf "%s Pkg: %.1f%% %s Incr: %.1f%% %s Tot: %.1f%% %s" pkg.name
511 (display_percent pkg.size) (display_size pkg.size)
512 (display_percent increm) (display_size increm)
513 (display_percent total) (display_size total)
518 let txt = pango_small_context#create_layout in
519 Pango.Layout.set_text txt (
520 sprintf "%s %.1f%% %.1f%% %.1f%%" pkg.name
521 (display_percent pkg.size)
522 (display_percent increm)
523 (display_percent total)
528 let txt = pango_small_context#create_layout in
529 Pango.Layout.set_text txt (
530 sprintf "%s" pkg.name
534 let txts = [ txt1; txt2; txt3; txt4; txt5; txt6; txt7; txt8 ] in
536 let fore = choose_contrasting_colour colour in
538 let rec loop = function
541 let txt = Lazy.force txt in
542 let { Pango.width = txtwidth;
543 Pango.height = txtheight } =
544 Pango.Layout.get_pixel_extent txt in
545 (* Now with added fudge-factor. *)
546 if width >= txtwidth + 8 && height >= txtheight + 8 then
547 draw#put_layout ~x:(x+4) ~y:(y+4) ~fore txt
552 and draw_pkg_narrow x y width height colour =
553 draw#set_foreground colour;
554 draw#rectangle ~x:(x+2) ~y:(y+2)
555 ~width:(width-4) ~height:(height-4) ~filled:true ()
557 and choose_contrasting_colour = function
559 if r + g + b > 98304 then `BLACK else `WHITE
562 and darken = function
564 `RGB (r * 9 / 10, g * 9 / 10, b * 9 / 10)
569 (* Return false because this is a Gtk event handler. *)
572 ignore (da#event#connect#expose ~callback:repaint);
575 let x, y = GdkEvent.Motion.x ev, GdkEvent.Motion.y ev in
577 let kill_tooltip () =
578 (match !tooltips with
580 | Some (tt : GData.tooltips) ->
581 tt#set_tip ~text:"" (da :> GObj.widget);
587 (match get_locn x y with
590 | Some (colour, pkg, total, increm) ->
591 (* The only way to make the tooltip follow the mouse is to
592 * kill the whole tooltips object and recreate it each time ...
595 let tt = GData.tooltips ~delay:100 () in
596 (* Tooltip text is the same as txt1. *)
597 let text = sprintf "%s
598 Package: %.1f%% %s (%Ld bytes)
599 Incremental: %.1f%% %s (%Ld bytes)
600 Total: %.1f%% %s (%Ld bytes)" pkg.nevra
601 (display_percent pkg.size) (display_size pkg.size) pkg.size
602 (display_percent increm) (display_size increm) increm
603 (display_percent total) (display_size total) total in
604 tt#set_tip ~text (da :> GObj.widget);
609 (* Return false because this is a Gtk event handler. *)
613 ignore (da#event#connect#motion_notify ~callback:motion);