mirror of
https://github.com/cookiengineer/audacity
synced 2025-04-29 15:19:44 +02:00
388 lines
12 KiB
Perl
Executable File
388 lines
12 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
|
|
use strict 'vars';
|
|
use File::Spec;
|
|
|
|
my $traceLevel = 3;
|
|
|
|
# whether to box the clusters by sub-folder, but always color nodes regardless
|
|
my @clusterlist = qw(
|
|
/xml
|
|
/export
|
|
/menus
|
|
/effects/VST
|
|
/effects/ladspa
|
|
/effects/lv2
|
|
/effects/nyquist
|
|
/effects/vamp
|
|
);
|
|
my %clusters;
|
|
@clusters{@clusterlist} = ();
|
|
sub clustering
|
|
{
|
|
return exists( $clusters{ $_[0] } );
|
|
}
|
|
|
|
# whether to prune redundant arcs implied in transitive closure
|
|
my $pruning = 1;
|
|
|
|
# whether to insert hyperlinks
|
|
my $links = 1;
|
|
|
|
# Step 1: collect short names and paths to .cpp files
|
|
# We assume that final path components uniquely identify the files!
|
|
my $dir = "../src";
|
|
|
|
my %names; # string to string
|
|
{
|
|
foreach my $file (`find $dir -name '*.cpp' -o -name '*.h' -o -name '*.mm'`) {
|
|
my $short = $file;
|
|
chop $short;
|
|
$short =~ s|\.cpp$||;
|
|
$short =~ s|\.h$||;
|
|
$short =~ s|\.mm$||;
|
|
my $shorter = ($short =~ s|^.*/||r);
|
|
$names{$shorter} = $short;
|
|
}
|
|
}
|
|
|
|
#my $linkroot = "https://github.com/audacity/audacity/tree/master/src";
|
|
my $linkroot = "file://" . File::Spec->rel2abs( $dir );
|
|
|
|
|
|
print STDERR "Found ", scalar( keys %names ), " filename(s)\n" if $traceLevel >= 1;
|
|
|
|
# Step 2: collect inclusions in each .cpp/.h pair, and folder information,
|
|
# and build a graph
|
|
my $arcs = 0;
|
|
my %graph; # hash from names to sets of names
|
|
my $grepcmd = "grep '^ *# *include[^\"]*\"[^\"]*\\.h\"'"; # find include directives with quotes
|
|
my $sedcmd = "sed -E 's|^[^\"]*\"([^\"]*)\\.h\".*\$|\\1|'"; # extract quoted path
|
|
my %folders; # build our own tree like the directories
|
|
my $nFolders = 1;
|
|
while( my ($shorter, $short) = each(%names) ) {
|
|
# find relevant files (.cpp and .h, and sometimes .mm too)
|
|
my $pat = "${short}.*";
|
|
my @files = glob $pat;
|
|
|
|
# store path information, for subgraph clustering later
|
|
$short = substr $short, length( $dir ) + 1;
|
|
my @ownComponents = split '/', $short;
|
|
my $last = pop @ownComponents;
|
|
my $folder = \%folders;
|
|
# this improves the graph in some ways:
|
|
# files that we just put directly under src should be treated as if in
|
|
# a separate subfolder.
|
|
@ownComponents = ("UNCLASSIFIED") if not @ownComponents;
|
|
# store paths in a hash from strings to references to hashes from strings to references to ...
|
|
# (ensuring a nonempty set at key "" for each node of this tree)
|
|
while (@ownComponents) {
|
|
my $component = shift @ownComponents;
|
|
if (not exists $$folder{ $component }) {
|
|
my %empty = ("",());
|
|
$$folder{ $component } = \%empty;
|
|
++$nFolders;
|
|
}
|
|
$folder = $$folder{ $component };
|
|
}
|
|
# at the last folder, hash empty string specially, to the set of files
|
|
if (not exists $$folder{ "" }) {
|
|
my %empty = ("",());
|
|
$$folder{ "" } = \%empty;
|
|
}
|
|
$$folder{""}{$last} = ();
|
|
|
|
my %empty;
|
|
$graph{$shorter} = \%empty; # be sure leaf nodes are not omitted from hash
|
|
foreach (`cat @files | $grepcmd | $sedcmd`) {
|
|
chop;
|
|
my @components = split '/';
|
|
my $include = $components[-1];
|
|
# omit self-arcs and arcs to .h files external to the project
|
|
if (($shorter ne $include) && (exists $names{$include})) {
|
|
$graph{$shorter}{$include} = (), ++$arcs;
|
|
}
|
|
}
|
|
}
|
|
|
|
print STDERR "Found ", scalar( keys %graph ), " node(s) and ${arcs} arc(s)\n" if $traceLevel >= 1;
|
|
|
|
# Step 3: compute an acyclic quotient graph
|
|
|
|
my %quotientMap; # from node name to reference to array of node names
|
|
|
|
sub SCCID {
|
|
# given reference to an array of names
|
|
# use the first name in the array as an ID
|
|
my $scc = shift;
|
|
return $$scc[0];
|
|
}
|
|
|
|
sub SCCLabel {
|
|
# given reference to an array of names
|
|
# use concatenation of names as the displayed label
|
|
my $scc = shift;
|
|
return join "\n", @$scc;
|
|
}
|
|
|
|
my %quotientGraph; # to be populated, from SCC ID to array of:
|
|
# [ array of immediately reachable SCC IDs,
|
|
# array of transitively reachable SCC ids,
|
|
# rank number ]
|
|
# The first member may be pruned to only those nodes reachable by a longest
|
|
# path of length one
|
|
|
|
# find strongly connected components with Tarjan's algorithm, which discovers
|
|
# the nodes of the quotient graph in a bottom-up topologically sorted order
|
|
my %temp; # assigns numbers to node names
|
|
my $count = 1;
|
|
my @stack; # names
|
|
my $traceDepth = 0;
|
|
$arcs = 0;
|
|
my $prunedArcs = 0;
|
|
my $maxRank = -1;
|
|
my $largest = 0;
|
|
# three utility procedures for discovery of one s.c.c.
|
|
sub merge {
|
|
my ($a, $b) = @_;
|
|
my $na = @$a;
|
|
my $nb = @$b;
|
|
my @result;
|
|
while ($na && $nb) {
|
|
if ($$a[-$na] lt $$b[-$nb]) {
|
|
push @result, $$a[-($na--)];
|
|
}
|
|
elsif ($$b[-$nb] lt $$a[-$na]) {
|
|
push @result, $$b[-($nb--)];
|
|
}
|
|
else {
|
|
push @result, $$a[-($na--)]; $nb--;
|
|
}
|
|
}
|
|
push @result, $$a[-($na--)] while $na;
|
|
push @result, $$b[-($nb--)] while $nb;
|
|
@result;
|
|
}
|
|
sub diff {
|
|
my ($a, $b) = @_;
|
|
my $na = @$a;
|
|
my $nb = @$b;
|
|
my @result;
|
|
while ($na && $nb) {
|
|
if ($$a[-$na] lt $$b[-$nb]) {
|
|
push @result, $$a[-($na--)];
|
|
}
|
|
elsif ($$b[-$nb] lt $$a[-$na]) {
|
|
$nb--;
|
|
}
|
|
else {
|
|
$na--; $nb--;
|
|
}
|
|
}
|
|
push @result, $$a[-($na--)] while $na;
|
|
@result;
|
|
}
|
|
sub discoverOneComponent {
|
|
my ($sorted, $traceIndent) = @_; # reference to sorted array of names
|
|
# first populate the quotient map
|
|
foreach my $node (@$sorted) {
|
|
$quotientMap{ $node } = $sorted;
|
|
}
|
|
# now add arcs to the quotient graph
|
|
my $qhead = $$sorted[0]; # identifier of quotient node, agreeing with sub SCCID
|
|
$#{$quotientGraph{ $qhead }} = 2; # reserve results
|
|
my $data = $quotientGraph{ $qhead }; # reference to results
|
|
my $rank = -1;
|
|
my @reachable;
|
|
my %direct;
|
|
my @merged;
|
|
foreach my $node (@$sorted) {
|
|
my $tails = $graph{ $node };
|
|
foreach my $tail ( keys %$tails ) {
|
|
# it is guaranteed that all destination nodes are already in quotientMap,
|
|
# because of the bottom-up discovery sequence, so this works:
|
|
my $qtail = SCCID( $quotientMap{ $tail } );
|
|
$direct{ $qtail } = () if ( $qhead ne $qtail );
|
|
my $tailData = $quotientGraph{ $qtail };
|
|
my $tailRank = $$tailData[2];
|
|
$rank = $tailRank if $tailRank > $rank;
|
|
@reachable = merge( $$tailData[1], \@reachable );
|
|
}
|
|
}
|
|
++$rank;
|
|
my @direct = sort ( keys %direct ); # all direct arcs
|
|
my @pruned = diff( \@direct, \@reachable ); # all nonredundant direct arcs
|
|
$prunedArcs += @pruned; # count for trace information
|
|
$arcs += @direct; # count for trace information
|
|
@reachable = merge( \@pruned, \@reachable ); # all nodes reachable (excluding self)
|
|
$$data[0] = $pruning ? \@pruned : \@direct;
|
|
$$data[1] = \@reachable;
|
|
$$data[2] = $rank;
|
|
if ($traceLevel >= 3) {
|
|
print STDERR "${traceIndent}${qhead}";
|
|
print STDERR " and ", (scalar(@$sorted) - 1), " other(s)" if scalar(@$sorted) > 1;
|
|
print STDERR " discovered at rank ${rank}\n";
|
|
}
|
|
$maxRank = $rank if $rank > $maxRank;
|
|
$largest = @$sorted if @$sorted > $largest;
|
|
}
|
|
#recursive procedure
|
|
sub tarjan {
|
|
my ($name, $num) = @_;
|
|
my $traceIndent = " " x $traceDepth;
|
|
if ( exists( $temp{$name} ) ) {
|
|
# have visited
|
|
my $number = $temp{$name};
|
|
if ($number > 0) {
|
|
#scc not fully known
|
|
print STDERR "${traceIndent}${name} ${number} revisited\n" if $traceLevel >= 3;
|
|
return $number;
|
|
}
|
|
else {
|
|
#scc known
|
|
return $num; # unchanged
|
|
}
|
|
}
|
|
else {
|
|
# first visit
|
|
push @stack, $name;
|
|
$temp{$name} = my $number = $count++;
|
|
print STDERR "${traceIndent}${name} ${number} discovering\n" if $traceLevel >= 3;
|
|
|
|
# recur on directly reachable nodes
|
|
my $least = $number;
|
|
my $tails = $graph{$name};
|
|
++$traceDepth;
|
|
foreach my $name2 ( keys %$tails ) {
|
|
my $result = tarjan( $name2, $number );
|
|
$least = $result if $result < $least;
|
|
}
|
|
--$traceDepth;
|
|
|
|
if ($least == $number) {
|
|
# finished a component (this was the first discovered node in it)
|
|
my $node;
|
|
my @scc;
|
|
do {
|
|
$node = pop @stack;
|
|
$temp{ $node } = 0;
|
|
push @scc, $node;
|
|
} while( $node ne $name );
|
|
my @sorted = sort @scc;
|
|
discoverOneComponent( \@sorted, $traceIndent );
|
|
return $num; # unchanged
|
|
}
|
|
else {
|
|
# not finished
|
|
print STDERR "${traceIndent}${name} deferred to ${least}\n" if $traceLevel >= 3;
|
|
return $least;
|
|
}
|
|
}
|
|
}
|
|
# top invocation of recursive procedure discovers all
|
|
foreach my $node ( keys %graph ) {
|
|
tarjan( $node, 0 );
|
|
}
|
|
#give trace information
|
|
if ($traceLevel >= 1) {
|
|
print STDERR "Found ", scalar(keys(%quotientGraph)), " strongly connected component(s) in ", (1 + $maxRank), " rank(s)\n";
|
|
print STDERR "Largest component size is ${largest}\n";
|
|
print STDERR "${arcs} arc(s) found (${prunedArcs} after pruning)\n";
|
|
}
|
|
|
|
# Step 4: output the graph in dot language
|
|
print STDERR "Generating .dot file\n" if $traceLevel >= 1;
|
|
|
|
# temporary redirection
|
|
*OLD_STDOUT = *STDOUT;
|
|
my $fname = "graph.dot";
|
|
open my $fh, ">", $fname or die "Can't open file";
|
|
*STDOUT = $fh;
|
|
|
|
# header
|
|
my $graphAttr =
|
|
# $clustering ?
|
|
"labeljust=l labelloc=b"
|
|
# : ""
|
|
;
|
|
print "strict digraph{ graph [";
|
|
print $graphAttr;
|
|
print " newrank=true";
|
|
#print " mclimit=0.01";
|
|
#print " nslimit=1";
|
|
#print " rank=max";
|
|
#print " rankdir=LR";
|
|
print "]\n";
|
|
print "node [style=filled]";
|
|
|
|
# nodes and their clusters
|
|
# group the nodes into subgraphs corresponding to directories
|
|
print "\n";
|
|
print "// Nodes\n";
|
|
|
|
my $hue = 0;
|
|
my $saturation = 1.0;
|
|
my $huestep = 1.0 / $nFolders;
|
|
sub subgraph{
|
|
my ($foldername, $hashref) = @_;
|
|
my $clustered = clustering( $foldername );
|
|
my $cluster = $clustered ? "cluster" : "";
|
|
my $clusterAttr = $clustered ? "style=bold color=\"blue\"" : "";
|
|
print STDERR "subgraph \"$foldername\"\n" if $traceLevel >= 3;
|
|
my $color = "${hue},${saturation},1.0";
|
|
$hue += $huestep;
|
|
$saturation = 1.5 - $saturation; # alternate bold and pale
|
|
my $attrs = $clusterAttr . "label=\"$foldername\"";
|
|
print "\nsubgraph \"${cluster}${foldername}\" { $attrs node [fillcolor=\"${color}\"]\n";
|
|
# describe the nodes at this level, stored as a set (i.e. a hash to
|
|
# don't-care values) at key ""
|
|
foreach my $name (sort (keys %{$$hashref{""}})) {
|
|
next unless $name; # ignore dummy element
|
|
my $scc = $quotientMap{ $name };
|
|
my $id = SCCID( $scc );
|
|
# only want the name that is the representative of its s.c.c.
|
|
# equivalence class
|
|
next unless $name eq $id;
|
|
my $label = SCCLabel( $scc );
|
|
print " \"${id}\" [label=\"$label\"";
|
|
# insert other node attributes here as key=value pairs,
|
|
print " URL=\"${linkroot}${foldername}/${id}.cpp\"" if $links;
|
|
# separated by spaces
|
|
print"]\n";
|
|
}
|
|
# now recur, to describe nested clusters
|
|
foreach my $name ( sort( keys %$hashref ) ) {
|
|
next unless $name; # we just did the special entry at key "" above,
|
|
# which is a set of leaves at this level, not a subtree
|
|
subgraph( "${foldername}/${name}", $$hashref{ $name } );
|
|
}
|
|
print "}\n";
|
|
}
|
|
subgraph( "", \%folders );
|
|
|
|
# now describe the arcs
|
|
print "\n";
|
|
print "// Arcs\n";
|
|
|
|
while( my ($head, $data) = each( %quotientGraph ) ) {
|
|
foreach my $tail ( @{$$data[0]} ) {
|
|
print " \"$head\" -> \"$tail\" [";
|
|
# insert arc attributes here as key=value pairs,
|
|
print "penwidth=2.0";
|
|
# separated by spaces
|
|
print"]\n";
|
|
}
|
|
}
|
|
|
|
#footer
|
|
print "}\n";
|
|
|
|
# restore
|
|
*STDOUT = *OLD_STDOUT;
|
|
|
|
# Step 5: generate image
|
|
print STDERR "Generating image...\n" if $traceLevel >= 1;
|
|
my $verbosity = ($traceLevel >= 2) ? "-v" : "";
|
|
`dot $verbosity -O -Tsvg $fname`;
|
|
print STDERR "done\n" if $traceLevel >= 1;
|