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 (* Open the window. *)
286 let title = root ^ " - Fedora RPM dependency size viewer" in
288 GWindow.window ~width:800 ~height:600 ~title ~allow_shrink:true () in
290 ignore (window#connect#destroy ~callback:GMain.quit);
292 let da = GMisc.drawing_area ~packing:window#add () in
294 let draw = new GDraw.drawable da#misc#window in
296 (* Pango contexts used to draw large and small text. *)
297 let pango_large_context = da#misc#create_pango_context in
298 pango_large_context#set_font_description (Pango.Font.from_string "Sans 12");
299 let pango_small_context = da#misc#create_pango_context in
300 pango_small_context#set_font_description (Pango.Font.from_string "Sans 8");
303 (* Get the canvas size and fill the background with white. *)
304 let width, height = draw#size in
305 draw#set_background `WHITE;
306 draw#set_foreground `WHITE;
307 draw#rectangle ~x:0 ~y:0 ~width ~height ~filled:true ();
309 (* Calculate the scales so we can fit everything into the window. *)
310 let Tree (_, top_total, top_increm, _, _) = tree in
311 assert (top_total = top_increm);
312 let rowheight = float height /. float depth in
313 let scale = float width /. Int64.to_float top_increm in
315 (* Now draw the tree. *)
316 let rec draw_tree y x = function
317 | Tree (pkg, total, increm, colour, children) ->
318 (* Draw pkg at (x, y). *)
319 let width = scale *. Int64.to_float increm in
320 let pkgsizewidth = scale *. Int64.to_float pkg.size in
321 draw_pkg x y width pkgsizewidth rowheight colour pkg total increm;
323 (* Draw the children of pkg at (i, y + rowheight), where
324 * i starts as x and increments for each child.
326 let y = y +. rowheight in
327 let rec loop x = function
329 | child :: children ->
331 let Tree (_, _, increm, _, _) = child in
332 let childwidth = scale *. Int64.to_float increm in
333 loop (x +. childwidth) children
337 (* Draw a single package. *)
338 and draw_pkg x y width pkgsizewidth height colour pkg total increm =
339 let x = int_of_float x in
340 let y = int_of_float y in
341 let width = int_of_float width in
342 let pkgsizewidth = int_of_float pkgsizewidth in
343 let height = int_of_float height in
346 draw_pkg_outline x y width pkgsizewidth height colour;
347 draw_pkg_label x y width height colour pkg total increm
349 else if width >= 4 then
350 draw_pkg_narrow x y width height colour
352 XXX This doesn't work. We need to coalesce small packages
354 draw_pkg_narrow x y 1 height colour *)
356 and draw_pkg_outline x y width pkgsizewidth height colour =
357 draw#set_foreground colour;
358 draw#rectangle ~x:(x+2) ~y:(y+2)
359 ~width:(width-4) ~height:(height-4)
361 if pkgsizewidth > 2 then (
362 draw#set_foreground (darken colour);
363 draw#rectangle ~x:(x+2) ~y:(y+2)
364 ~width:(pkgsizewidth-2) ~height:(height-4)
366 draw#set_foreground (choose_contrasting_colour colour);
367 draw#set_line_attributes ~style:`ON_OFF_DASH ();
368 draw#line (x+pkgsizewidth) (y+2) (x+pkgsizewidth) (y+height-2);
369 draw#set_line_attributes ~style:`SOLID ()
371 draw#set_foreground (`BLACK);
372 draw#rectangle ~x:(x+2) ~y:(y+2)
373 ~width:(width-4) ~height:(height-4)
376 and draw_pkg_label x y width height colour pkg total increm =
377 (* How to write text in a drawing area, in case it's not
378 * obvious, which it certainly is not:
379 * http://www.math.nagoya-u.ac.jp/~garrigue/soft/olabl/lablgtk-list/120.txt
382 let txt = pango_large_context#create_layout in
383 Pango.Layout.set_text txt (
385 Package: %.1f%% %s (%Ld bytes)
386 Incremental: %.1f%% %s (%Ld bytes)
387 Total: %.1f%% %s (%Ld bytes)" pkg.nevra
388 (display_percent pkg.size) (display_size pkg.size) pkg.size
389 (display_percent increm) (display_size increm) increm
390 (display_percent total) (display_size total) total
395 let txt = pango_small_context#create_layout in
396 Pango.Layout.set_text txt (
398 Package: %.1f%% %s (%Ld bytes)
399 Incremental: %.1f%% %s (%Ld bytes)
400 Total: %.1f%% %s (%Ld bytes)" pkg.nevra
401 (display_percent pkg.size) (display_size pkg.size) pkg.size
402 (display_percent increm) (display_size increm) increm
403 (display_percent total) (display_size total) total
408 let txt = pango_small_context#create_layout in
409 Pango.Layout.set_text txt (
411 Pkg: %.1f%% %s (%Ld bytes)
412 Incr: %.1f%% %s (%Ld bytes)
413 Tot: %.1f%% %s (%Ld bytes)" pkg.name
414 (display_percent pkg.size) (display_size pkg.size) pkg.size
415 (display_percent increm) (display_size increm) increm
416 (display_percent total) (display_size total) total
421 let txt = pango_small_context#create_layout in
422 Pango.Layout.set_text txt (
426 Tot: %.1f%% %s" pkg.name
427 (display_percent pkg.size) (display_size pkg.size)
428 (display_percent increm) (display_size increm)
429 (display_percent total) (display_size total)
434 let txt = pango_small_context#create_layout in
435 Pango.Layout.set_text txt (
436 sprintf "%s\nPkg: %.1f%%\nIncr: %.1f%%\nTot: %.1f%%"
438 (display_percent pkg.size)
439 (display_percent increm)
440 (display_percent total)
445 let txt = pango_small_context#create_layout in
446 Pango.Layout.set_text txt (
447 sprintf "%s Pkg: %.1f%% %s Incr: %.1f%% %s Tot: %.1f%% %s" pkg.name
448 (display_percent pkg.size) (display_size pkg.size)
449 (display_percent increm) (display_size increm)
450 (display_percent total) (display_size total)
455 let txt = pango_small_context#create_layout in
456 Pango.Layout.set_text txt (
457 sprintf "%s %.1f%% %.1f%% %.1f%%" pkg.name
458 (display_percent pkg.size)
459 (display_percent increm)
460 (display_percent total)
465 let txt = pango_small_context#create_layout in
466 Pango.Layout.set_text txt (
467 sprintf "%s" pkg.name
471 let txts = [ txt1; txt2; txt3; txt4; txt5; txt6; txt7; txt8 ] in
473 let fore = choose_contrasting_colour colour in
475 let rec loop = function
478 let txt = Lazy.force txt in
479 let { Pango.width = txtwidth;
480 Pango.height = txtheight } =
481 Pango.Layout.get_pixel_extent txt in
482 (* Now with added fudge-factor. *)
483 if width >= txtwidth + 8 && height >= txtheight + 8 then
484 draw#put_layout ~x:(x+4) ~y:(y+4) ~fore txt
489 and draw_pkg_narrow x y width height colour =
490 draw#set_foreground colour;
491 draw#rectangle ~x:(x+2) ~y:(y+2)
492 ~width:(width-4) ~height:(height-4) ~filled:true ()
494 and choose_contrasting_colour = function
496 if r + g + b > 98304 then `BLACK else `WHITE
499 and darken = function
501 `RGB (r * 9 / 10, g * 9 / 10, b * 9 / 10)
504 and display_percent bytes =
505 100. *. Int64.to_float bytes /. Int64.to_float top_total
507 and display_size bytes =
508 if bytes > 104_857L then
509 sprintf "%.1f MB" (Int64.to_float bytes /. 1_048_576.)
510 else if bytes > 102L then
511 sprintf "%.1f KB" (Int64.to_float bytes /. 1_024.)
515 draw_tree 0. 0. tree;
517 (* Return false because this is a Gtk event handler. *)
520 ignore (da#event#connect#expose ~callback:repaint);