├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── demo ├── k9s-perfetto.png ├── k9s-s3.dot ├── k9s-s3.svg ├── k9s-trace.json └── k9s.json ├── go.mod ├── go.sum ├── graph.go ├── main.go ├── top.go ├── tree.go └── types.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: "1.20" 19 | 20 | - name: Build 21 | run: go build -v ./... 22 | 23 | - name: Test 24 | run: go test -v ./... 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ravelin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # actiongraph 2 | 3 | > 👋 We wrote this to figure out why our CI had slowed down a bit and found 4 | > something was accidentally compiling in much more Kubernetes than an offline 5 | > CLI needed. If you enjoy writing performant Go too, please do considering 6 | > joining us in the fight against fraud: [ravelin.com/careers]. 7 | 8 | `actiongraph` is a CLI for investigating where `go build` is spending its time 9 | compiling. It consumes the file written using the compile.json output by `go 10 | build -debug-actiongraph=compile.json`, which includes information about compile 11 | steps and dependencies between packages. 12 | 13 | `actiongraph` can help you identify which packages take a lot of compilation 14 | time with `top`, summarise where that time is spent with `tree`, and identify 15 | where the dependencies come from with `graph --why`. 16 | 17 | Alongside the `-debug-actiongraph` flag is the `-debug-trace` flag which this 18 | program does not use, but is a similarly instructive tool which can help you 19 | identify parallelism and build order. For completeness, it is also shown below. 20 | 21 | > ℹ Both the `-debug-actiongraph` and `-debug-trace` flags are not supported and 22 | > deliberately not documented by the Go team: 23 | > https://github.com/golang/go/commit/8fce59eab5cb2facfafca89e047b4b43ba44785f. 24 | 25 | ## Installation 26 | 27 | go install github.com/icio/actiongraph@latest 28 | 29 | ## Usage overview 30 | 31 | # Optionally clear your go build cache, if you're wanting to understanding why 32 | # a build in CI is taking a while (depending on its cache reuse): 33 | go clean -cache 34 | 35 | # Compile your program using the undocumented -debug-actiongraph flag: 36 | go build -debug-actiongraph=compile.json ./my-prog 37 | 38 | # Show the slowest individual packages: 39 | actiongraph top -f compile.json 40 | 41 | # Show aggregate time spent compiling nested packages: 42 | actiongraph tree -f compile.json 43 | 44 | # Show aggregate time spent compiling github packages: 45 | actiongraph tree -f compile.json -L 2 github.com 46 | 47 | # Render dependency diagrams of packages, focusing on why PKG was compiled in: 48 | actiongraph graph --why PKG -f compile.json > compile-pkg.dot 49 | dot -Tsvg -Grankdir=LR < compile-pkg.dot > compile-pkg.svg 50 | 51 | ## Worked example 52 | 53 | In this example, we're going to look inside one of @icio's favourite CLIs, 54 | https://k9scli.io/. 55 | 56 | First, let's check that we're all set up: 57 | 58 | go version 59 | actiongraph -h 60 | 61 | then create our demo space: 62 | 63 | mkdir demo 64 | cd demo 65 | 66 | We want to investigate a fresh build, so let's clear the build cache: 67 | 68 | go clean -cache 69 | 70 | Now we compile. We're going to specify body `-debug-actiongraph` and `-debug-trace` here so we can compare the results. (And we're going to send the compiled binary into a temporary directory.) 71 | 72 | GOBIN=$(mktemp -d) go install -debug-actiongraph=k9s.json -debug-trace=k9s-trace.json github.com/derailed/k9s@v0.27.4 73 | 74 | We should have a new file called `k9s.json` describing the actiongraph. Let's take a look at the first few entries `jq '.[:5] | .[]' -c < k9s.json`: 75 | 76 | {"ID":0,"Mode":"go install","Package":"","Deps":[1],"Priority":1190,"TimeReady":"2023-05-12T09:24:39.647607494+01:00","TimeStart":"2023-05-12T09:24:39.647610965+01:00","TimeDone":"2023-05-12T09:24:39.647611018+01:00","Cmd":null} 77 | {"ID":1,"Mode":"link-install","Package":"github.com/derailed/k9s","Deps":[2],"Objdir":"/tmp/go-build3966251150/b001/","Target":"/tmp/tmp.WIZyR7yX3Y/k9s","Priority":1189,"Built":"/tmp/tmp.WIZyR7yX3Y/k9s","BuildID":"LZKyYv0RT8N-_YjcRQ1R/PsuartwNDnqAz2PYjtcW/lvWIs0IgoEnXVRitK8yx/ClYoHVGOEFtTZ-Gnzq2h","TimeReady":"2023-05-12T09:24:39.647340894+01:00","TimeStart":"2023-05-12T09:24:39.647348289+01:00","TimeDone":"2023-05-12T09:24:39.647607252+01:00","Cmd":null} 78 | {"ID":2,"Mode":"link","Package":"github.com/derailed/k9s","Deps":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188],"Objdir":"/tmp/go-build3966251150/b001/","Target":"/tmp/go-build3966251150/b001/exe/a.out","Priority":1188,"Built":"/tmp/go-build3966251150/b001/exe/a.out","ActionID":"LZKyYv0RT8N-_YjcRQ1R","BuildID":"LZKyYv0RT8N-_YjcRQ1R/PsuartwNDnqAz2PYjtcW/lvWIs0IgoEnXVRitK8yx/ClYoHVGOEFtTZ-Gnzq2h","TimeReady":"2023-05-12T09:24:33.62117428+01:00","TimeStart":"2023-05-12T09:24:33.621181576+01:00","TimeDone":"2023-05-12T09:24:39.647340491+01:00","Cmd":["/home/icio/sdk/go/pkg/tool/linux_amd64/link -o /tmp/go-build3966251150/b001/exe/a.out -importcfg /tmp/go-build3966251150/b001/importcfg.link -X=runtime.godebugDefault=panicnil=1 -buildmode=exe -buildid=LZKyYv0RT8N-_YjcRQ1R/PsuartwNDnqAz2PYjtcW/lvWIs0IgoEnXVRitK8yx/LZKyYv0RT8N-_YjcRQ1R -extld=gcc /tmp/go-build3966251150/b001/_pkg_.a"],"CmdReal":5757072143,"CmdUser":6457235000,"CmdSys":853740000} 79 | {"ID":3,"Mode":"build","Package":"github.com/derailed/k9s","Deps":[4,5,6,7,8,1189],"Objdir":"/tmp/go-build3966251150/b001/","Priority":1187,"NeedBuild":true,"ActionID":"PsuartwNDnqAz2PYjtcW","BuildID":"PsuartwNDnqAz2PYjtcW/lvWIs0IgoEnXVRitK8yx","TimeReady":"2023-05-12T09:24:33.604116889+01:00","TimeStart":"2023-05-12T09:24:33.604117711+01:00","TimeDone":"2023-05-12T09:24:33.621174013+01:00","Cmd":["/home/icio/sdk/go/pkg/tool/linux_amd64/compile -o /tmp/go-build3966251150/b001/_pkg_.a -trimpath \"/tmp/go-build3966251150/b001=>\" -p main -lang=go1.20 -complete -buildid PsuartwNDnqAz2PYjtcW/PsuartwNDnqAz2PYjtcW -c=4 -nolocalimports -importcfg /tmp/go-build3966251150/b001/importcfg -pack /home/icio/go/pkg/mod/github.com/derailed/k9s@v0.27.4/main.go"],"CmdReal":15611237,"CmdUser":16273000} 80 | {"ID":4,"Mode":"build","Package":"flag","Deps":[9,10,11,12,13,14,8,15,16,17,18],"Objdir":"/tmp/go-build3966251150/b002/","Priority":44,"NeedBuild":true,"ActionID":"fhRdV7jbdbpj3WwhqK7M","BuildID":"fhRdV7jbdbpj3WwhqK7M/fpr490Yv8t_PFttE-9YV","TimeReady":"2023-05-12T09:23:48.146874388+01:00","TimeStart":"2023-05-12T09:23:48.146904614+01:00","TimeDone":"2023-05-12T09:23:48.29620602+01:00","Cmd":["/home/icio/sdk/go/pkg/tool/linux_amd64/compile -o /tmp/go-build3966251150/b002/_pkg_.a -trimpath \"/tmp/go-build3966251150/b002=>\" -p flag -std -complete -buildid fhRdV7jbdbpj3WwhqK7M/fhRdV7jbdbpj3WwhqK7M -c=4 -nolocalimports -importcfg /tmp/go-build3966251150/b002/importcfg -pack /home/icio/sdk/go/src/flag/flag.go"],"CmdReal":140280033,"CmdUser":144616000,"CmdSys":22834000} 81 | 82 | So let's find the compile steps that took the longest: 83 | 84 | $ actiongraph -f k9s.json top 85 | 9.016s 2.75% build k8s.io/api/core/v1 86 | 6.026s 4.59% link github.com/derailed/k9s 87 | 5.071s 6.13% build github.com/aws/aws-sdk-go/service/s3 88 | 3.620s 7.23% build github.com/aws/aws-sdk-go/aws/endpoints 89 | 3.474s 8.29% build net/http 90 | 3.215s 9.27% build net 91 | 2.869s 10.15% build github.com/google/gnostic/openapiv2 92 | 2.846s 11.02% build github.com/google/gnostic/openapiv3 93 | 2.581s 11.80% build k8s.io/apimachinery/pkg/apis/meta/v1 94 | 2.476s 12.56% build google.golang.org/protobuf/internal/impl 95 | 2.186s 13.22% build github.com/gogo/protobuf/proto 96 | 1.974s 13.83% build k8s.io/api/extensions/v1beta1 97 | 1.919s 14.41% build github.com/derailed/tview 98 | 1.898s 14.99% build k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1 99 | 1.840s 15.55% build github.com/klauspost/compress/zstd 100 | 1.796s 16.10% build github.com/derailed/k9s/internal/view 101 | 1.671s 16.61% build k8s.io/kubectl/pkg/describe 102 | 1.658s 17.11% build github.com/prometheus/procfs 103 | 1.624s 17.61% build k8s.io/api/apps/v1 104 | 1.596s 18.09% build runtime 105 | 106 | By default, `actiongraph top` will show the 20 slowest steps. This may be 107 | overridden using the `-n0` flag. For example, to get the fastest steps we could 108 | check: 109 | 110 | $ actiongraph -f k9s.json top -n0 | tail -5 111 | 0.006s 100.00% build google.golang.org/protobuf/internal/flags 112 | 0.000s 100.00% link-install github.com/derailed/k9s 113 | 0.000s 100.00% nop 114 | 0.000s 100.00% built-in package unsafe 115 | 0.000s 100.00% go install 116 | 117 | Here we can see that the second column is showing the cumulative percentage of 118 | time spent up to that package. 119 | 120 | We can summarise the time spent within package domain/directories using the 121 | `tree` subcommand: 122 | 123 | $ actiongraph -f k9s.json tree -L 1 124 | 322.013s (root) 125 | 154.673s k8s.io 126 | 83.357s github.com 127 | 40.294s std 128 | 12.338s sigs.k8s.io 129 | 10.886s golang.org 130 | 7.178s google.golang.org 131 | 5.413s helm.sh 132 | 2.428s gopkg.in 133 | 2.344s go.starlark.net 134 | 1.702s go.opentelemetry.io 135 | 1.400s oras.land 136 | 137 | We can see that we spent 322 seconds compiling k9s, though some of that compilation would have happened in parallel, and so the 138 | 139 | We've rolled up all standard libraries under `std`: 140 | 141 | $ actiongraph -f k9s.json tree encoding 142 | 322.013s (root) 143 | 40.294s std 144 | 1.942s 0.017s std/encoding 145 | 0.572s 0.572s std/encoding/json 146 | 0.564s 0.564s std/encoding/xml 147 | 0.207s 0.207s std/encoding/asn1 148 | 0.192s 0.192s std/encoding/binary 149 | 0.096s 0.096s std/encoding/csv 150 | 0.080s 0.080s std/encoding/hex 151 | 0.074s 0.074s std/encoding/base64 152 | 0.073s 0.073s std/encoding/pem 153 | 0.067s 0.067s std/encoding/base32 154 | 155 | Let's look at which github repos are taking the longest to compile: 156 | 157 | $ actiongraph -f k9s.json tree github.com -L 2 | head -15 158 | 322.013s (root) 159 | 83.357s github.com 160 | 17.307s github.com/aws 161 | 17.307s github.com/aws/aws-sdk-go 162 | 16.360s github.com/derailed 163 | 7.674s 0.017s github.com/derailed/k9s 164 | 4.517s github.com/derailed/popeye 165 | 2.250s github.com/derailed/tcell 166 | 1.919s 1.919s github.com/derailed/tview 167 | 8.206s github.com/google 168 | 6.524s github.com/google/gnostic 169 | 1.014s github.com/google/go-cmp 170 | 0.277s 0.277s github.com/google/btree 171 | 0.163s 0.115s github.com/google/gofuzz 172 | 0.150s 0.150s github.com/google/uuid 173 | 174 | We saw from `top` that package github.com/aws/aws-sdk-go/service/s3 was one of the slowest to compile. To understand why, we're going to use the `graph` subcommand which can filter down the dependency list to highlight all import paths leading from our build target to the package indicated by `--why PKG`: 175 | 176 | actiongraph -f k9s.json graph --why github.com/aws/aws-sdk-go/service/s3 > k9s-s3.dot 177 | 178 | and then render it using [Graphviz](https://graphviz.org/)'s `dot`: 179 | 180 | dot -Tsvg < k9s-s3.dot > k9s-s3.svg 181 | 182 | which looks something like this: 183 | 184 | ![actiongraph graph --why github.com/aws/aws-sdk-go/service/s3 -f demo/k9s-s3.json](demo/k9s-s3.svg) 185 | 186 | Lastly we can open our k9s-trace.json in https://ui.perfetto.dev/ or Google 187 | Chrome/Chromium's chrome://tracing. This shows us how successfully the scheduler 188 | utilises the multiple cores on our machine when compiling each package: 189 | 190 | ![k9s-trace.json in perfetto](demo/k9s-perfetto.png) 191 | 192 | At the time of writing, `-debug-trace` is under development in 193 | https://github.com/golang/go/issues/38714. 194 | 195 | [ravelin.com/careers]: https://www.ravelin.com/careers?utm_medium=referral&utm_source=github&utm_campaign=actiongraph 196 | -------------------------------------------------------------------------------- /demo/k9s-perfetto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icio/actiongraph/3cf5d9b3415761ba1efef5d97b2289f4190e0b8c/demo/k9s-perfetto.png -------------------------------------------------------------------------------- /demo/k9s-s3.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | 3 [label=<github.com/derailed
k9s
build 17.056302ms>; shape=box]; 3 | 3 -> 5; 4 | 5 [label=<github.com/derailed/k9s
cmd
build 170.344979ms>; shape=box]; 5 | 5 -> 22; 6 | 5 -> 23; 7 | 22 [label=<github.com/derailed/k9s/internal
ui
build 750.135239ms>; shape=box]; 8 | 22 -> 98; 9 | 22 -> 99; 10 | 23 [label=<github.com/derailed/k9s/internal
view
build 1.795657517s>; shape=box]; 11 | 23 -> 98; 12 | 23 -> 99; 13 | 23 -> 112; 14 | 23 -> 22; 15 | 23 -> 115; 16 | 23 -> 117; 17 | 98 [label=<github.com/derailed/k9s/internal
dao
build 788.569378ms>; shape=box]; 18 | 98 -> 277; 19 | 99 [label=<github.com/derailed/k9s/internal
model
build 444.912435ms>; shape=box]; 20 | 99 -> 98; 21 | 99 -> 117; 22 | 112 [label=<github.com/derailed/k9s/internal
perf
build 43.795665ms>; shape=box]; 23 | 112 -> 98; 24 | 115 [label=<github.com/derailed/k9s/internal/ui
dialog
build 41.487591ms>; shape=box]; 25 | 115 -> 22; 26 | 117 [label=<github.com/derailed/k9s/internal
xray
build 345.505127ms>; shape=box]; 27 | 117 -> 98; 28 | 277 [label=<github.com/derailed/popeye
pkg
build 364.880583ms>; shape=box]; 29 | 277 -> 526; 30 | 526 [label=<github.com/aws/aws-sdk-go/service/s3
s3manager
build 356.912126ms>; shape=box]; 31 | 526 -> 700; 32 | 526 -> 701; 33 | 700 [label=<github.com/aws/aws-sdk-go/service
s3
build 5.070865122s>; shape=box]; 34 | 701 [label=<github.com/aws/aws-sdk-go/service/s3
s3iface
build 444.346913ms>; shape=box]; 35 | 701 -> 700; 36 | } 37 | -------------------------------------------------------------------------------- /demo/k9s-s3.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | 13 | 14 | 3 15 | 16 | github.com/derailed 17 | k9s 18 | build 17.056302ms 19 | 20 | 21 | 22 | 5 23 | 24 | github.com/derailed/k9s 25 | cmd 26 | build 170.344979ms 27 | 28 | 29 | 30 | 3->5 31 | 32 | 33 | 34 | 35 | 36 | 22 37 | 38 | github.com/derailed/k9s/internal 39 | ui 40 | build 750.135239ms 41 | 42 | 43 | 44 | 5->22 45 | 46 | 47 | 48 | 49 | 50 | 23 51 | 52 | github.com/derailed/k9s/internal 53 | view 54 | build 1.795657517s 55 | 56 | 57 | 58 | 5->23 59 | 60 | 61 | 62 | 63 | 64 | 98 65 | 66 | github.com/derailed/k9s/internal 67 | dao 68 | build 788.569378ms 69 | 70 | 71 | 72 | 22->98 73 | 74 | 75 | 76 | 77 | 78 | 99 79 | 80 | github.com/derailed/k9s/internal 81 | model 82 | build 444.912435ms 83 | 84 | 85 | 86 | 22->99 87 | 88 | 89 | 90 | 91 | 92 | 23->22 93 | 94 | 95 | 96 | 97 | 98 | 23->98 99 | 100 | 101 | 102 | 103 | 104 | 23->99 105 | 106 | 107 | 108 | 109 | 110 | 112 111 | 112 | github.com/derailed/k9s/internal 113 | perf 114 | build 43.795665ms 115 | 116 | 117 | 118 | 23->112 119 | 120 | 121 | 122 | 123 | 124 | 115 125 | 126 | github.com/derailed/k9s/internal/ui 127 | dialog 128 | build 41.487591ms 129 | 130 | 131 | 132 | 23->115 133 | 134 | 135 | 136 | 137 | 138 | 117 139 | 140 | github.com/derailed/k9s/internal 141 | xray 142 | build 345.505127ms 143 | 144 | 145 | 146 | 23->117 147 | 148 | 149 | 150 | 151 | 152 | 277 153 | 154 | github.com/derailed/popeye 155 | pkg 156 | build 364.880583ms 157 | 158 | 159 | 160 | 98->277 161 | 162 | 163 | 164 | 165 | 166 | 99->98 167 | 168 | 169 | 170 | 171 | 172 | 99->117 173 | 174 | 175 | 176 | 177 | 178 | 112->98 179 | 180 | 181 | 182 | 183 | 184 | 115->22 185 | 186 | 187 | 188 | 189 | 190 | 117->98 191 | 192 | 193 | 194 | 195 | 196 | 526 197 | 198 | github.com/aws/aws-sdk-go/service/s3 199 | s3manager 200 | build 356.912126ms 201 | 202 | 203 | 204 | 277->526 205 | 206 | 207 | 208 | 209 | 210 | 700 211 | 212 | github.com/aws/aws-sdk-go/service 213 | s3 214 | build 5.070865122s 215 | 216 | 217 | 218 | 526->700 219 | 220 | 221 | 222 | 223 | 224 | 701 225 | 226 | github.com/aws/aws-sdk-go/service/s3 227 | s3iface 228 | build 444.346913ms 229 | 230 | 231 | 232 | 526->701 233 | 234 | 235 | 236 | 237 | 238 | 701->700 239 | 240 | 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/icio/actiongraph 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/spf13/cobra v1.7.0 7 | golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 8 | ) 9 | 10 | require ( 11 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 12 | github.com/spf13/pflag v1.0.5 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 3 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 6 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 7 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 8 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 9 | golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= 10 | golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /graph.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func addGraphCommand(prog *cobra.Command) { 12 | cmd := cobra.Command{ 13 | GroupID: "actiongraph", 14 | Use: "graph [-f compile.json] [--why PKG]", 15 | Short: "Graphviz visaualisation of the build steps", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | opt, err := loadOptions(cmd) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | why, err := cmd.Flags().GetString("why") 23 | if err != nil { 24 | return err 25 | } 26 | 27 | return graph(opt, why) 28 | }, 29 | } 30 | cmd.Flags().String("why", "", "show only paths to the given package") 31 | prog.AddCommand(&cmd) 32 | } 33 | 34 | func graph(opt *options, why string) error { 35 | actions := opt.actions 36 | 37 | // show is a shortcut set of actions with Deps leading to the destination. 38 | show := make([]int, len(actions)) 39 | shown := 0 40 | 41 | // Ignore "nop" nodes. 42 | for _, act := range actions { 43 | if act.Mode == "nop" { 44 | // TODO: What is the Mode:"nop" action? It typically has many Deps 45 | // that make rendering the graph complicated. 46 | show[act.ID] = avoid 47 | } 48 | } 49 | 50 | if why != "" { 51 | // Look for our destination node. 52 | for i, act := range actions { 53 | if act.Mode == "build" && act.Package == why { 54 | shown++ 55 | show[i] = follow 56 | break 57 | } 58 | } 59 | if shown == 0 { 60 | return fmt.Errorf("could not find package %q", why) 61 | } 62 | } 63 | 64 | if shown == 0 { 65 | // If there are no specific nodes we want, show them all. 66 | for i, g := range show { 67 | if g != avoid { 68 | show[i] = follow 69 | } 70 | } 71 | } else if shown > 0 { 72 | // Find the first build step. 73 | start := -1 74 | for _, act := range actions { 75 | if act.Mode == "build" { 76 | start = act.ID 77 | break 78 | } 79 | } 80 | if start == -1 { 81 | return errors.New("no first build step") 82 | } 83 | 84 | // Show all nodes between the start and the other nodes we want to show. 85 | pathfind(start, show, func(n int) []int { return actions[n].Deps }) 86 | } 87 | 88 | fmt.Fprintln(opt.stdout, "digraph {") 89 | for i, g := range show { 90 | if g != follow { 91 | continue 92 | } 93 | act := actions[i] 94 | fmt.Fprintf(opt.stdout, "%d [label=<%s>; shape=box];\n", i, ""+filepath.Dir(act.Package)+"
"+filepath.Base(act.Package)+"
"+act.Mode+" "+act.TimeDone.Sub(act.TimeStart).String()) 95 | 96 | for _, dep := range act.Deps { 97 | if show[dep] != follow { 98 | continue 99 | } 100 | fmt.Printf("\t%d -> %d;\n", i, dep) 101 | } 102 | } 103 | fmt.Fprintln(opt.stdout, "}") 104 | 105 | return nil 106 | } 107 | 108 | const ( 109 | avoid = -1 110 | unknown = 0 111 | follow = 1 112 | ) 113 | 114 | func pathfind(start int, guide []int, edges func(int) []int) { 115 | stack := [][]int{{start}} 116 | for len(stack) > 0 { 117 | // Pop the stack. 118 | depth := len(stack) - 1 119 | n := stack[depth][0] 120 | 121 | switch guide[n] { 122 | case avoid: 123 | // Nothing. 124 | case unknown: 125 | // Step into the children. 126 | if deps := edges(n); len(deps) > 0 { 127 | stack = append(stack, deps) 128 | continue 129 | } 130 | case follow: 131 | // Mark the path to this point as followable. 132 | for i := range stack { 133 | guide[stack[i][0]] = follow 134 | } 135 | } 136 | 137 | // Trim the stack. 138 | for d := len(stack) - 1; d >= 0; d-- { 139 | s := stack[d] 140 | m := s[0] 141 | if guide[m] != follow { 142 | guide[m] = avoid 143 | } 144 | 145 | if len(s) == 1 { 146 | stack = stack[:d] 147 | continue 148 | } 149 | stack[d] = s[1:] 150 | break 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | txttpl "text/template" 11 | "time" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func main() { 17 | err := run(os.Args[1:]...) 18 | if err != nil { 19 | fmt.Fprintf(os.Stderr, "actiongraph: %s\n", err) 20 | os.Exit(1) 21 | } 22 | } 23 | 24 | func run(args ...string) error { 25 | prog := &cobra.Command{ 26 | Use: "actiongraph", 27 | SilenceUsage: true, 28 | SilenceErrors: true, 29 | } 30 | 31 | prog.PersistentFlags().StringP("file", "f", "-", "JSON file to read (use - for stdin)") 32 | prog.MarkFlagRequired("file") 33 | prog.RegisterFlagCompletionFunc("file", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 34 | return []string{"json"}, cobra.ShellCompDirectiveFilterFileExt 35 | }) 36 | 37 | addTopCommand(prog) 38 | addTreeCommand(prog) 39 | addTypesCommand(prog) 40 | addGraphCommand(prog) 41 | 42 | prog.AddGroup(&cobra.Group{ 43 | ID: "actiongraph", 44 | Title: "Actiongraph:", 45 | }) 46 | 47 | prog.SetArgs(args) 48 | return prog.Execute() 49 | } 50 | 51 | type options struct { 52 | stdin io.Reader 53 | stdout io.Writer 54 | args []string 55 | funcs txttpl.FuncMap 56 | actions []action 57 | total time.Duration 58 | } 59 | 60 | func loadOptions(cmd *cobra.Command) (*options, error) { 61 | opt := options{ 62 | stdin: cmd.InOrStdin(), 63 | stdout: cmd.OutOrStdout(), 64 | args: cmd.Flags().Args(), 65 | 66 | funcs: txttpl.FuncMap{ 67 | "base": filepath.Base, 68 | "dir": filepath.Dir, 69 | "seconds": func(d time.Duration) string { 70 | return fmt.Sprintf("%.3fs", d.Seconds()) 71 | }, 72 | "percent": func(v float64) string { 73 | return fmt.Sprintf("%.2f%%", v) 74 | }, 75 | "right": func(n int, s string) string { 76 | if len(s) > n { 77 | return s 78 | } 79 | return strings.Repeat(" ", n-len(s)) + s 80 | }, 81 | }, 82 | } 83 | 84 | // Open the actiongraph JSON file. 85 | fn, err := cmd.Flags().GetString("file") 86 | if err != nil { 87 | return nil, err 88 | } 89 | f, err := openFile(fn) 90 | if err != nil { 91 | return nil, err 92 | } 93 | defer f.Close() 94 | 95 | // Decode the actions. 96 | if err := json.NewDecoder(f).Decode(&opt.actions); err != nil { 97 | return nil, fmt.Errorf("decoding input: %w", err) 98 | } 99 | 100 | // A few top-level calculations. 101 | for i := range opt.actions { 102 | // TODO: Flag to look at CmdReal/CmdUser instead? We can use the Cmd 103 | // field being non-null to differentiate between cached and 104 | // non-cached steps, too. 105 | d := opt.actions[i].TimeDone.Sub(opt.actions[i].TimeStart) 106 | opt.actions[i].Duration = d 107 | opt.total += d 108 | } 109 | for i := range opt.actions { 110 | opt.actions[i].Percent = 100 * float64(opt.actions[i].Duration) / float64(opt.total) 111 | } 112 | return &opt, nil 113 | } 114 | 115 | func openFile(path string) (*os.File, error) { 116 | switch path { 117 | case "", "-", "/dev/stdin", "/dev/fd/0": 118 | return os.Stdin, nil 119 | default: 120 | return os.Open(path) 121 | } 122 | } 123 | 124 | type action struct { 125 | ID int 126 | Mode string 127 | Package string 128 | Deps []int 129 | Objdir string 130 | Target string 131 | Priority int 132 | Built string 133 | BuildID string 134 | TimeReady time.Time 135 | TimeStart time.Time 136 | TimeDone time.Time 137 | Cmd any 138 | ActionID string 139 | CmdReal int 140 | CmdUser int64 141 | CmdSys int 142 | NeedBuild bool 143 | 144 | Duration time.Duration 145 | Percent float64 146 | } 147 | -------------------------------------------------------------------------------- /top.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "text/template" 7 | "time" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func addTopCommand(cmd *cobra.Command) { 13 | topCmd := cobra.Command{ 14 | GroupID: "actiongraph", 15 | Use: "top [-f compile.json] [-n limit]", 16 | Short: "List slowest build steps", 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | opt, err := loadOptions(cmd) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | flags := cmd.Flags() 24 | limit, err := flags.GetInt("limit") 25 | if err != nil { 26 | return err 27 | } 28 | 29 | tplStr, err := flags.GetString("tpl") 30 | if err != nil { 31 | return err 32 | } 33 | tpl, err := template.New("top").Funcs(opt.funcs).Parse(tplStr) 34 | if err != nil { 35 | return fmt.Errorf("parsing tpl: %w", err) 36 | } 37 | 38 | return top(opt, limit, tpl) 39 | }, 40 | } 41 | flags := topCmd.Flags() 42 | flags.IntP("limit", "n", 20, "number of slowest build steps to show") 43 | flags.String("tpl", `{{ .Duration | seconds | right 8 }}{{ .CumulativePercent | percent | right 8 }} {{.Mode}} {{.Package}}`, "template for output") 44 | cmd.AddCommand(&topCmd) 45 | } 46 | 47 | func top(opt *options, limit int, tpl *template.Template) error { 48 | actions := opt.actions 49 | 50 | sort.Slice(actions, func(i, j int) bool { 51 | return actions[i].Duration >= actions[j].Duration 52 | }) 53 | 54 | var cum time.Duration 55 | for i, node := range actions { 56 | if limit > 0 && i >= limit { 57 | break 58 | } 59 | 60 | cum += node.Duration 61 | err := tpl.Execute(opt.stdout, topAction{ 62 | action: node, 63 | CumulativePercent: 100 * float64(cum) / float64(opt.total), 64 | }) 65 | if err != nil { 66 | return err 67 | } 68 | fmt.Fprintln(opt.stdout) 69 | } 70 | return nil 71 | } 72 | 73 | type topAction struct { 74 | action 75 | CumulativePercent float64 76 | } 77 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "text/template" 7 | "time" 8 | 9 | "github.com/spf13/cobra" 10 | "golang.org/x/exp/maps" 11 | "golang.org/x/exp/slices" 12 | ) 13 | 14 | func addTreeCommand(prog *cobra.Command) { 15 | cmd := cobra.Command{ 16 | GroupID: "actiongraph", 17 | Use: "tree [-m] [-f compile.json] [package...]", 18 | Short: "Total build times by directory", 19 | Args: cobra.ArbitraryArgs, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | opt, err := loadOptions(cmd) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | flags := cmd.Flags() 27 | level, err := flags.GetInt("level") 28 | if err != nil { 29 | return nil 30 | } 31 | 32 | tplStr, err := flags.GetString("tpl") 33 | if err != nil { 34 | return err 35 | } 36 | tpl, err := template.New("top").Funcs(opt.funcs).Parse(tplStr) 37 | if err != nil { 38 | return fmt.Errorf("parsing tpl: %w", err) 39 | } 40 | 41 | return tree(opt, level, args, tpl) 42 | }, 43 | } 44 | 45 | flags := cmd.Flags() 46 | flags.IntP("level", "L", -1, "descend only level directories deep (-ve for unlimited)") 47 | flags.String("tpl", `{{ .CumulativeDuration | seconds | right 8 }} {{ if eq .ID -1 }} {{ else }}{{ .Duration | seconds | right 8 }}{{ end }} {{.Indent}}{{.Package}}`, "template for output") 48 | 49 | prog.AddCommand(&cmd) 50 | } 51 | 52 | func tree(opt *options, level int, focus []string, tpl *template.Template) error { 53 | actions := opt.actions 54 | root := buildTree(actions) 55 | 56 | if len(focus) != 0 { 57 | filterActs := make([]action, len(focus)) 58 | for i, pkg := range focus { 59 | filterActs[i] = action{ 60 | ID: 0, // buildTree and pruneTree use -1 for intermediary nodes. 61 | Mode: "build", // buildTree ignores non-build actions. 62 | Package: strings.TrimRight(pkg, "/."), 63 | } 64 | } 65 | pruneTree(root, buildTree(filterActs)) 66 | } 67 | 68 | dirs := append(make([][]*pkgtree, 0, 10), []*pkgtree{root}) 69 | for len(dirs) > 0 { 70 | // Step up from empty paths. 71 | last := len(dirs) - 1 72 | if len(dirs[last]) == 0 { 73 | dirs = dirs[:last] 74 | continue 75 | } 76 | 77 | // Take the next node. 78 | n := dirs[last][0] 79 | dirs[last] = dirs[last][1:] 80 | if level >= 0 && n.depth > level { 81 | continue 82 | } 83 | 84 | // Display the node. 85 | node := treeAction{ 86 | ID: n.id, 87 | Package: n.path, 88 | Depth: n.depth, 89 | Indent: strings.Repeat(" ", last), 90 | CumulativePercent: 100 * float64(n.d) / float64(opt.total), 91 | CumulativeDuration: n.d, 92 | } 93 | if n.id > 0 { 94 | node.action = actions[n.id] 95 | } 96 | err := tpl.Execute(opt.stdout, node) 97 | if err != nil { 98 | return err 99 | } 100 | fmt.Fprintln(opt.stdout) 101 | 102 | // Step into the children. 103 | if len(n.dir) > 0 { 104 | kids := maps.Values(n.dir) 105 | slices.SortFunc(kids, func(a, b *pkgtree) bool { return a.d > b.d }) 106 | dirs = append(dirs, kids) 107 | continue 108 | } 109 | } 110 | return nil 111 | } 112 | 113 | type pkgtree struct { 114 | path string 115 | depth int 116 | d time.Duration 117 | id int 118 | 119 | dir map[string]*pkgtree 120 | } 121 | 122 | func buildTree(actions []action) *pkgtree { 123 | root := pkgtree{ 124 | path: "(root)", 125 | id: -1, 126 | } 127 | 128 | // Loop over each built package path. 129 | for _, act := range actions { 130 | if act.Mode != "build" { 131 | continue 132 | } 133 | 134 | // Assume all packages without a "." are part of the standard library. 135 | // TODO: Go modules don't need to start with a domain, so this is wrong. 136 | pkg := act.Package 137 | if isStdlib(pkg) { 138 | pkg = "std/" + pkg 139 | } 140 | 141 | // Create the tree of nodes for this one package. 142 | actNode := &root 143 | actNode.d += act.Duration 144 | p := 0 145 | depth := 0 146 | for more := true; more; { 147 | depth++ 148 | 149 | // Read the next deepest path from pkg. 150 | pn := strings.Index(pkg[p+1:], "/") 151 | if pn == -1 { 152 | p = len(pkg) 153 | more = false 154 | } else { 155 | p += pn + 1 156 | } 157 | path := pkg[:p] 158 | 159 | // Ensure a node for this path exists. 160 | if actNode.dir == nil { 161 | actNode.dir = make(map[string]*pkgtree, 1) 162 | } 163 | p := actNode.dir[path] 164 | if p == nil { 165 | p = &pkgtree{ 166 | id: -1, 167 | path: path, 168 | depth: depth, 169 | } 170 | actNode.dir[path] = p 171 | } 172 | 173 | // Descend into the node for this path. 174 | actNode = p 175 | actNode.d += act.Duration 176 | } 177 | 178 | actNode.id = act.ID 179 | } 180 | return &root 181 | } 182 | 183 | func isStdlib(pkg string) bool { 184 | root, _, _ := strings.Cut(pkg, "/") 185 | return !strings.Contains(root, ".") 186 | } 187 | 188 | // pruneTree removes dir grandchildren from root that are not in keep and resets 189 | // the depth according to the closest grandchild in keep. 190 | func pruneTree(root, keep *pkgtree) { 191 | type job struct{ r, k *pkgtree } 192 | work := make([]job, 0, 10) 193 | work = append(work, job{root, keep}) 194 | 195 | // TODO: actiongraph tree pkg1 pkg1/pkg2/pkg3 -L 0 should show 196 | // pkg1/pkg2/pkg3 but we don't currently traverse past pkg1. 197 | for len(work) > 0 { 198 | j := work[len(work)-1] 199 | work = work[:len(work)-1] 200 | 201 | // TODO: Can we consolidate these two cases? 202 | if j.k.id == -1 { 203 | // Branch nodes in keep have ID -1. We want to kepe the common 204 | // children of root and keep. 205 | for path, rChild := range j.r.dir { 206 | rChild.depth = 0 207 | kChild := j.k.dir[path] 208 | if kChild == nil { 209 | delete(j.r.dir, path) 210 | } else { 211 | work = append(work, job{rChild, kChild}) 212 | } 213 | } 214 | } else { 215 | // This path was explicitly added to keep. We want to reset the 216 | // depth of each child from this point. 217 | for path, rChild := range j.r.dir { 218 | rChild.depth -= j.k.depth 219 | kChild := j.k.dir[path] 220 | if kChild == nil { 221 | kChild = &pkgtree{ 222 | id: 0, 223 | path: path, 224 | depth: j.k.depth, // Keep this depth. 225 | } 226 | } 227 | work = append(work, job{rChild, kChild}) 228 | } 229 | } 230 | } 231 | } 232 | 233 | type treeAction struct { 234 | ID int 235 | Package string 236 | Indent string 237 | Depth int 238 | CumulativeDuration time.Duration 239 | CumulativePercent float64 240 | action 241 | } 242 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "text/template" 7 | "time" 8 | 9 | "golang.org/x/exp/maps" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func addTypesCommand(cmd *cobra.Command) { 15 | topCmd := cobra.Command{ 16 | GroupID: "actiongraph", 17 | Use: "types [-f compile.json] [-n limit]", 18 | Short: "List slowest action types", 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | opt, err := loadOptions(cmd) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | flags := cmd.Flags() 26 | 27 | tplStr, err := flags.GetString("tpl") 28 | if err != nil { 29 | return err 30 | } 31 | tpl, err := template.New("top").Funcs(opt.funcs).Parse(tplStr) 32 | if err != nil { 33 | return fmt.Errorf("parsing tpl: %w", err) 34 | } 35 | 36 | return typesTop(opt, tpl) 37 | }, 38 | } 39 | flags := topCmd.Flags() 40 | flags.String("tpl", `{{ .Duration | seconds | right 8 }}{{ .Percentage | percent | right 8 }} {{.Mode}}`, "template for output") 41 | cmd.AddCommand(&topCmd) 42 | } 43 | 44 | func typesTop(opt *options, tpl *template.Template) error { 45 | actions := opt.actions 46 | types := map[string]typesAction{} 47 | var cum time.Duration 48 | for _, node := range actions { 49 | cum += node.Duration 50 | ta, f := types[node.Mode] 51 | if !f { 52 | ta = typesAction{Mode: node.Mode} 53 | } 54 | ta.Duration += node.Duration 55 | ta.Percentage = 100 * float64(ta.Duration) / float64(opt.total) 56 | types[node.Mode] = ta 57 | } 58 | actionTypes := maps.Values(types) 59 | sort.Slice(actionTypes, func(i, j int) bool { 60 | return actionTypes[i].Duration >= actionTypes[j].Duration 61 | }) 62 | 63 | for _, node := range actionTypes { 64 | err := tpl.Execute(opt.stdout, node) 65 | if err != nil { 66 | return err 67 | } 68 | fmt.Fprintln(opt.stdout) 69 | } 70 | return nil 71 | } 72 | 73 | type typesAction struct { 74 | Mode string 75 | Duration time.Duration 76 | Percentage float64 77 | } 78 | --------------------------------------------------------------------------------