├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── LICENSE-THIRD-PARTY ├── NEWS.md ├── README.md ├── clip └── clip.go ├── cmd └── gotraceui │ ├── HACKING.md │ ├── argminmax.go │ ├── assets │ ├── assets.go │ └── data │ │ ├── dance-128.gif │ │ ├── dance-32.gif │ │ ├── dance-64.gif │ │ ├── logo-128.png │ │ └── logo-256.png │ ├── canvas.go │ ├── colors.go │ ├── debug.go │ ├── debug_shared.go │ ├── debug_stub.go │ ├── events.go │ ├── filter.go │ ├── flamegraph.go │ ├── float32.go │ ├── function.go │ ├── gc.go │ ├── goroutine.go │ ├── heatmap.go │ ├── histogram.go │ ├── items.go │ ├── labels.go │ ├── link.go │ ├── main.go │ ├── plot.go │ ├── processor.go │ ├── span.go │ ├── stack.go │ ├── statistics.go │ ├── task.go │ ├── testdata │ └── fuzz │ │ └── FuzzLoadTrace │ │ ├── 015cd8e72fab9610c5fac7a531c9d5fdb90e9847bae917d8632d81e180472b52 │ │ ├── 048e23791869d19df7d253944aa22adc8e864f6420813afc377fbb50cf20a249 │ │ ├── 1be32de1435cb5898c1b913b1bc1b14a90a9a5475ed5ee12850cbf3274e7adfb │ │ ├── 331cc7da77f3f5aecb99448b1bf73d42eed38e307b313eef231e1fb6e255812f │ │ ├── 61e0412ed82a1fedd57896cba5eec7ff5efc58917260e5b129f035c77535da71 │ │ ├── 78cdf18e1b5aca794c4b57fda7acf1303f3ebfdfd6a1397f012af6b6e4847b27 │ │ ├── 962d74bd35140192c61ba2e9c66392c00d8d4e87a47f5224085f25511c9e1245 │ │ ├── 9eed0f96b06667bd2b52f5795aaceccb5a29416e13ff603796727183a6d1c765 │ │ ├── a2fa581a51bdd48d070564e9bddd969e212748249f480c87867b5ae2e46aff45 │ │ ├── ae3ce2c64d2c41da │ │ ├── e9612e08a5083e9183cb225f0996c02c149a4c0dbb9d132cf4075db4b12fd9d2 │ │ ├── ecad2f142ca420d4 │ │ └── fc59395db74455370eb952640caccdda823925dcc450e007a9879088b7006967 │ ├── text.go │ ├── textures.go │ ├── timeline.go │ ├── trace.go │ ├── util.go │ └── version.go ├── color └── color.go ├── container ├── option.go ├── rb.go └── set.go ├── doc └── manual │ ├── LICENSE │ ├── images │ ├── logo.png │ ├── olive.jpg │ └── screenshots │ │ ├── flame-graph.png │ │ ├── heatmap-linear.png │ │ ├── heatmap-ranked.png │ │ ├── merged-span.png │ │ ├── pollable-io-span.png │ │ ├── sampling.png │ │ ├── track_whiskers_spans_end.png │ │ └── user-regions.png │ ├── manual.org │ └── style.css ├── f32color └── rgba.go ├── flake.lock ├── flake.nix ├── font ├── README ├── fallback.ttf ├── font.go └── runes.txt ├── generate_icons.sh ├── gesture ├── alias.go └── gesture.go ├── go.mod ├── go.sum ├── images └── icon.png ├── layout ├── alias.go ├── layout.go └── list.go ├── mem └── mem.go ├── mysync └── sync.go ├── seq └── seq.go ├── share ├── applications │ └── co.honnef.Gotraceui.desktop ├── icons │ └── hicolor │ │ ├── 1024x1024 │ │ └── apps │ │ │ └── gotraceui.png │ │ ├── 128x128 │ │ └── apps │ │ │ └── gotraceui.png │ │ ├── 192x192 │ │ └── apps │ │ │ └── gotraceui.png │ │ ├── 256x256 │ │ └── apps │ │ │ └── gotraceui.png │ │ ├── 32x32 │ │ └── apps │ │ │ └── gotraceui.png │ │ ├── 48x48 │ │ └── apps │ │ │ └── gotraceui.png │ │ ├── 512x512 │ │ └── apps │ │ │ └── gotraceui.png │ │ ├── 64x64 │ │ └── apps │ │ │ └── gotraceui.png │ │ ├── 72x72 │ │ └── apps │ │ │ └── gotraceui.png │ │ ├── 96x96 │ │ └── apps │ │ │ └── gotraceui.png │ │ └── scalable │ │ └── apps │ │ └── gotraceui.svg └── mime │ └── packages │ └── x-gotraceui.xml ├── slices └── slices.go ├── theme ├── animation.go ├── cmdpalette.go ├── component.go ├── dialog.go ├── editor.go ├── flamegraph.go ├── future.go ├── histogram.go ├── label.go ├── list.go ├── list_window.go ├── menu.go ├── modal.go ├── panel.go ├── scrollbar.go ├── table.go ├── theme.go ├── util.go └── window.go ├── tinylfu ├── LICENSE ├── cm4.go ├── cm4_test.go ├── doorkeeper.go ├── internal │ └── list │ │ └── list.go ├── lru.go ├── s2lru.go ├── tinylfu.go └── tinylfu_test.go ├── trace └── ptrace │ ├── debug.go │ ├── nodebug.go │ ├── pattern.go │ ├── ptrace.go │ ├── statistics.go │ └── validate.go ├── unsafe └── unsafe.go └── widget ├── alias.go ├── bool.go ├── button.go ├── flamegraph.go ├── gif.go ├── histogram.go └── widget.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: dominikh 2 | github: dominikh 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /cmd/gotraceui/gotraceui 2 | /cmd/gotraceui/gotraceui.test 3 | /_traces 4 | /doc/manual/manual.pdf 5 | /doc/manual/gotraceui.pdf 6 | /doc/manual/*.log 7 | /doc/manual/*.tuc 8 | /doc/manual/*.toc 9 | /doc/manual/*.out 10 | /doc/manual/*.aux 11 | /doc/manual/*.bbl 12 | /doc/manual/*.bcf 13 | /doc/manual/*.blg 14 | /doc/manual/*.fdb_latexmk 15 | /doc/manual/*.fls 16 | /doc/manual/*.run.xml 17 | /gotraceui.pdf 18 | *.pprof 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dominik Honnef 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 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # v0.5.0 (unreleased) 2 | 3 | - Introduce thin strips above tracks that indicate tiny spans, merged spans, events, and 4 | CPU samples. This moves events out of the main track. 5 | - Add tooltips for spans in GC and STW timelines 6 | - Add shortcuts for opening traces and quitting Gotraceui 7 | - Don't slow down UI trying to compute accurate span durations 8 | - Display time spans in addition to (or in place of) durations 9 | - Display durations of flame graph spans in proportion to their parents, zoomed-on roots, and global roots. 10 | - Fix the display of call stack depth in the tooltips of stack spans 11 | - Reduce memory required to load traces by 20-30% 12 | - Reduce time required to load traces by 10-30% 13 | - Implement redo for navigations 14 | - Add support for traces produced by Go 1.22 15 | - Processor timelines more accurately represent processor states 16 | 17 | 18 | # v0.4.0 (2024-01-09) 19 | 20 | - Require Go 1.21 for building Gotraceui 21 | - All tables now have resizable and sortable columns 22 | - Added flame graphs 23 | - Links to timelines and spans now default to opening information 24 | - Links that open things and links that navigate use different colors (red and blue, respectively) 25 | - Clicking on spans in GC and STW timelines no longer crashes 26 | - Hovering table rows highlights them 27 | - When hovering timestamp links, their targets will be indicated in the timelines view 28 | - Spans have a new context menu item, to open their span information 29 | - Switch to a tabbed user interface instead of using multiple windows 30 | - Added a Goroutines tab that lists all goroutines in the trace 31 | - Panels can be converted into tabs 32 | - Added context menu to goroutine labels in timelines view 33 | - No longer create empty GC or STW timelines 34 | - Merged spans now display a detailed representation of the contained spans 35 | - Fixed a rare crash at startup 36 | - Fixed a rare crash for some traces 37 | - Handle DPI changes while Gotraceui is running 38 | - Display user-friendly strings instead of `` for unknown timestamps 39 | - Hovered and highlighted spans are now indicated with a gradient instead of the border color 40 | - Span borders more accurately show if the span's beginning or end are outside the visible area 41 | - Merged spans no longer stop at the screen boundaries 42 | - Lowered peak memory usage 43 | - "active" spans in goroutine timelines now have a label 44 | - Improved rendering of graphs with many points 45 | 46 | 47 | # v0.3.0 (2023-08-07) 48 | 49 | - Added support for traces produced by Go 1.21 50 | - Improved the formatting of durations in statistics 51 | - Made minor improvements to visual appearance 52 | - Replaced foldables with a tabbed interface 53 | - Allow selecting and copying stack traces 54 | - Added list of spans to goroutine panel 55 | - Added parent goroutine to goroutine panel 56 | - Added statistics to spans panel 57 | - Added individual spans' stack traces in spans panel 58 | - Added function panel 59 | - Navigating to timestamps now places them at the configured axis origin 60 | - Fixed ctrl+scroll zooming on Windows 61 | - Fixed a crash when loading traces that start in the middle of a task 62 | - Truncate long span labels instead of hiding them 63 | - Added a button to select all instances of a user region 64 | - Added histograms 65 | - Stop-the-world spans now display the reason for the STW 66 | - Made various performance improvements 67 | - Drastically lowered memory usage for traces with a lot of deep call stacks 68 | - Holding shift while scrolling vertically will cause horizontal scrolling, and vice versa 69 | - The info panel of user regions now displays events that occurred on the goroutine during said regions 70 | - Improved accuracy of computation of durations of merged spans 71 | - Added more features to the context menu of span links 72 | - Expensive computations, such as creating goroutine statistics, now run in the background and no longer prevent the UI from updating. 73 | 74 | 75 | # v0.2.0 (2023-04-11) 76 | 77 | - Added a button for copying statistics as CSV 78 | - Added context menu to links to processors 79 | - Added a list of spans to the span panel for merged spans 80 | - Added display of span tags in span panel 81 | - Added a dialog for highlighting spans based on their states 82 | - Automatically turn off the memory graph's "auto-extents" feature when manually resetting the extents 83 | - Syscall spans now include the syscall's name in the span label 84 | - Toggling compact mode now keeps the timeline under the cursor at its position 85 | - Disabling stack tracks or compact mode ensures the cursor stays on the current timeline 86 | - Improved the formatting of durations in statistics 87 | - Minor performance improvements 88 | - Fixed the display of the ⌘ sign in shortcuts 89 | - Fixed displaying context menus in additional windows 90 | - Fixed crash when clicking on link to processor 91 | - The checkboxes for filtering event lists now wrap to multiple lines if necessary 92 | - Minor improvements to the manual 93 | 94 | 95 | # v0.1.0 (2023-03-30) 96 | 97 | - Initial release. 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gotraceui - an efficient frontend for Go execution traces 2 | 3 | Gotraceui is a tool for visualizing and analyzing Go execution traces. It is meant to be a faster, more accessible, and 4 | more powerful alternative to `go tool trace`. Unlike `go tool trace`, Gotraceui doesn’t use deprecated browser APIs (or a 5 | browser at all), and its UI is tuned specifically to the unique characteristics of Go traces. 6 | 7 | ![Screenshot](https://github.com/dominikh/gotraceui/assets/39825/794d4b51-7c73-4ab7-b652-12b57ef38130) 8 | 9 | ## Installation 10 | 11 | Users of Nix can use the flake. There are no packages for other distributions or OSs yet and you will have to build 12 | `honnef.co/go/gotraceui/cmd/gotraceui` yourself. The manual contains information on how to. 13 | 14 | ## Manual 15 | 16 | - [Manual for the latest release](https://gotraceui.dev/manual/latest/) 17 | - [Manual for the development version](https://gotraceui.dev/manual/master/) 18 | 19 | ## Notes for package maintainers 20 | 21 | When packaging Gotraceui please take care to 22 | 23 | - pass `-X gioui.org/app.ID=co.honnef.Gotraceui` to the linker 24 | - install the `share` directory 25 | - call the package `gotraceui`, _please_ 26 | - include the `LICENSE-THIRD-PARTY` file; it contains all the licenses and copyright notices of all dependencies and all 27 | code our code is derived from. Including this file satisfies the requirement of reproducing copyright notices and 28 | permission notices. 29 | 30 | ## License 31 | 32 | The source code of the program and all assets necessary to run the program are licensed under the MIT license. 33 | The manual (all files in `doc/manual` as well as the compiled output) is licensed under the [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). 34 | 35 | Copies of the licenses of all dependencies can be found in LICENSE-THIRD-PARTY. 36 | 37 | ## Copyright 38 | 39 | All original work is copyrighted by its respective authors (consult the git log.) 40 | Parts of the code are derived from Go, © The Go Authors. 41 | Parts of the code are derived from Gio, © The Gio authors. 42 | Parts of the code are derived from go-tinylfu, © Damian Gryski 43 | `font/fallback.ttf` is derived from the DejaVu fonts, © Bitstream, © Tavmjong Bah 44 | `doc/manual/images/olive.jpg` is © Charlotte Brandhorst-Satzkorn, photographer and owner of the subject. 45 | 46 | The compiled binary includes code from dependencies. These dependencies and their copyright holders can be found in `LICENSE-THIRD-PARTY`. 47 | 48 | ## Known issues 49 | 50 | - [runtime/trace: time stamps out of order](https://github.com/golang/go/issues/16755) 51 | -------------------------------------------------------------------------------- /clip/clip.go: -------------------------------------------------------------------------------- 1 | package clip 2 | 3 | import ( 4 | "math" 5 | 6 | "gioui.org/f32" 7 | "gioui.org/op" 8 | "gioui.org/op/clip" 9 | ) 10 | 11 | type Ellipse = clip.Ellipse 12 | type Op = clip.Op 13 | type Outline = clip.Outline 14 | type Path = clip.Path 15 | type PathSpec = clip.PathSpec 16 | type RRect = clip.RRect 17 | type Rect = clip.Rect 18 | type Stack = clip.Stack 19 | type Stroke = clip.Stroke 20 | 21 | type FRect struct { 22 | Min f32.Point 23 | Max f32.Point 24 | } 25 | 26 | func (r FRect) Path(ops *op.Ops) clip.PathSpec { 27 | var p clip.Path 28 | p.Begin(ops) 29 | r.IntoPath(&p) 30 | return p.End() 31 | } 32 | 33 | func (r FRect) IntoPath(p *clip.Path) { 34 | p.MoveTo(r.Min) 35 | p.LineTo(f32.Pt(r.Max.X, r.Min.Y)) 36 | p.LineTo(r.Max) 37 | p.LineTo(f32.Pt(r.Min.X, r.Max.Y)) 38 | p.LineTo(r.Min) 39 | } 40 | 41 | func (r FRect) IntoPathR(p *clip.Path) { 42 | p.MoveTo(r.Min) 43 | p.LineTo(f32.Pt(r.Min.X, r.Max.Y)) 44 | p.LineTo(r.Max) 45 | p.LineTo(f32.Pt(r.Max.X, r.Min.Y)) 46 | p.LineTo(r.Min) 47 | } 48 | 49 | func (r FRect) Op(ops *op.Ops) clip.Op { 50 | return clip.Outline{Path: r.Path(ops)}.Op() 51 | } 52 | 53 | func (r FRect) Contains(pt f32.Point) bool { 54 | return pt.X >= r.Min.X && pt.X < r.Max.X && 55 | pt.Y >= r.Min.Y && pt.Y < r.Max.Y 56 | } 57 | 58 | func (r FRect) Dx() float32 { 59 | return r.Max.X - r.Min.X 60 | } 61 | 62 | func (r FRect) Dy() float32 { 63 | return r.Max.Y - r.Min.Y 64 | } 65 | 66 | type RectangularOutline struct { 67 | Rect FRect 68 | Width float32 69 | } 70 | 71 | func (out RectangularOutline) Op(ops *op.Ops) clip.Op { 72 | var p clip.Path 73 | p.Begin(ops) 74 | out.Rect.IntoPath(&p) 75 | inner := FRect{ 76 | Min: f32.Pt(out.Rect.Min.X+out.Width, out.Rect.Min.Y+out.Width), 77 | Max: f32.Pt(out.Rect.Max.X-out.Width, out.Rect.Max.Y-out.Width), 78 | } 79 | inner.IntoPathR(&p) 80 | p.Close() 81 | 82 | return clip.Outline{Path: p.End()}.Op() 83 | } 84 | 85 | // FRRect represents the clip area of a rectangle with rounded 86 | // corners. 87 | // 88 | // Specify a square with corner radii equal to half the square size to 89 | // construct a circular clip area. 90 | type FRRect struct { 91 | Rect FRect 92 | // The corner radii. 93 | SE, SW, NW, NE float32 94 | } 95 | 96 | // Op returns the op for the rounded rectangle. 97 | func (rr FRRect) Op(ops *op.Ops) Op { 98 | return Outline{Path: rr.Path(ops)}.Op() 99 | } 100 | 101 | // Push the rectangle clip on the clip stack. 102 | func (rr FRRect) Push(ops *op.Ops) Stack { 103 | return rr.Op(ops).Push(ops) 104 | } 105 | 106 | func (rr FRRect) IntoPath(p *clip.Path) { 107 | // https://pomax.github.io/bezierinfo/#circles_cubic. 108 | const q = 4 * (math.Sqrt2 - 1) / 3 109 | const iq = 1 - q 110 | 111 | se, sw, nw, ne := rr.SE, rr.SW, rr.NW, rr.NE 112 | rrf := FRect{ 113 | Min: f32.Pt(rr.Rect.Min.X, rr.Rect.Min.Y), 114 | Max: f32.Pt(rr.Rect.Max.X, rr.Rect.Max.Y), 115 | } 116 | w, n, e, s := rrf.Min.X, rrf.Min.Y, rrf.Max.X, rrf.Max.Y 117 | 118 | p.MoveTo(f32.Point{X: w + nw, Y: n}) 119 | p.LineTo(f32.Point{X: e - ne, Y: n}) // N 120 | p.CubeTo( // NE 121 | f32.Point{X: e - ne*iq, Y: n}, 122 | f32.Point{X: e, Y: n + ne*iq}, 123 | f32.Point{X: e, Y: n + ne}) 124 | p.LineTo(f32.Point{X: e, Y: s - se}) // E 125 | p.CubeTo( // SE 126 | f32.Point{X: e, Y: s - se*iq}, 127 | f32.Point{X: e - se*iq, Y: s}, 128 | f32.Point{X: e - se, Y: s}) 129 | p.LineTo(f32.Point{X: w + sw, Y: s}) // S 130 | p.CubeTo( // SW 131 | f32.Point{X: w + sw*iq, Y: s}, 132 | f32.Point{X: w, Y: s - sw*iq}, 133 | f32.Point{X: w, Y: s - sw}) 134 | p.LineTo(f32.Point{X: w, Y: n + nw}) // W 135 | p.CubeTo( // NW 136 | f32.Point{X: w, Y: n + nw*iq}, 137 | f32.Point{X: w + nw*iq, Y: n}, 138 | f32.Point{X: w + nw, Y: n}) 139 | 140 | } 141 | 142 | // Path returns the PathSpec for the rounded rectangle. 143 | func (rr FRRect) Path(ops *op.Ops) PathSpec { 144 | var p Path 145 | p.Begin(ops) 146 | rr.IntoPath(&p) 147 | return p.End() 148 | } 149 | 150 | func UniformFRRect(rect FRect, radius float32) FRRect { 151 | return FRRect{ 152 | Rect: rect, 153 | SE: radius, 154 | SW: radius, 155 | NE: radius, 156 | NW: radius, 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /cmd/gotraceui/HACKING.md: -------------------------------------------------------------------------------- 1 | This document contains notes on the tricky parts of processing runtime traces. 2 | 3 | # Syscalls 4 | 5 | When user code invokes a syscall, the runtime emits an EvGoSysCall event. If the syscall returns before sysmon finds 6 | the P that's blocked in the syscall, we'll not see any other events. This is a fast path, avoiding a bunch of 7 | scheduling, allowing the goroutine to resume immediately after the syscall returns, having retained its P and M. 8 | 9 | If sysmon does find the P, it retakes the P from the M and the runtime emits GoSysBlock, followed by ProcStop. When a 10 | blocking syscall returns, we get an EvGoSysExit event. The EvGoSysExit event gets emitted right before the G resumes 11 | running. However, to maintain accurate timing, the event carries the original timestamp of when the syscall returned, 12 | and the trace parser uses this to reorder the event. However, the fact that a G emits EvGoSysExit means that we'll 13 | also see an EvProcStart before the EvGoSysExit, in preparation for the GoStart that will follow after EvGoSysExit. 14 | This EvProcStart event matters to us because it might be for the same M that the syscall was running on. In that 15 | case, we'll have to adjust some start and end times, so that the EvProcStart and EvGoSysExit don't overlap. 16 | Concretely, the syscall span has to finish before the P span starts. 17 | 18 | One implication of sysmon detecting blocked syscalls is that the duration between EvGoSysCall and EvGoSysBlock should 19 | be attributed to the syscall, too. This is the time between starting the syscall and Go figuring out that it's 20 | blocking. However, for Ps, it very much matters if the syscall hasn't been detected as blocking yet. If it hasn't, 21 | the P isn't available for scheduling other Gs; this constitutes a form of latency. Because of that, we shouldn't 22 | simply extend the "blocked syscall" span to include the EvGoSysCall, but it might make sense to display the interval 23 | between the two as its own state. 24 | 25 | Sysmon runs at most every 20 μs, but it will run considerably less often if it thinks there's nothing to do, up to 10 26 | ms. In testing, even in busy programs (with GOMAXPROCS restricted to low values), sysmon sometimes decides to sleep 27 | for 5 ms. A loop calling syscall.Nanosleep, sleeping 10 ms each time, will have EvGoSysBlock spans that last from ~10 28 | ms to 5 ms, with the other 5 ms being the time between EvGoSysCall and EvGoSysBlock. 29 | 30 | Another issue is that when a syscall returns fast enough we'll not be provided with information on how long the 31 | syscall took. In the worst case, a fast syscall could've run for 10 ms, and to us it'll look like 10 ms of user code. 32 | 33 | One special case is syscall.RawSyscall, which doesn't emit any tracing events whatsoever, and AFAIU isn't handled by 34 | sysmon. That means these syscalls can block Ps and Ms, but we have no insight into it happening. 35 | 36 | # Trace consistency 37 | 38 | Tracing can start and stop in the middle of the program's lifetime, repeatedly. The following corner cases can arise 39 | due to this (not an exhaustive list): 40 | 41 | - The user stops tracing before ending a user region or user task. This means we can't rely on all create events 42 | having valid links to end events. 43 | 44 | - The user stops tracing before all goroutines have ended. This happens virtually all the time for the runtime.main 45 | goroutine. 46 | 47 | - User task IDs are not reset between traces. 48 | 49 | - The user starts a region before starting tracing, and ends it after starting tracing. runtime/trace handles this 50 | for us; StartRegion returns a "no-op region" if tracing isn't enabled, ending which doesn't emit any event. 51 | 52 | - The user starts tracing, starts a region, stops tracing, starts tracing, stops the region. If we only look at the 53 | second trace, then we see that the region ends, but we never see its creation. 54 | 55 | - The same tracing states as in the previous two examples, but starting and ending tasks instead of regions. For 56 | tasks, we can see the end of unknown tasks in both cases, because there are no "no-op tasks". 57 | 58 | - The user creates a task with a parent task that we didn't see, either because tracing wasn't enabled or because the 59 | parent was enabled in a previous trace. 60 | 61 | # Garbage collection 62 | 63 | - The only use of p=1000004 is for GCStart 64 | - GCDone happens on other procs, probably whichever proc was running the background worker that determined that we're done 65 | - A similar thing applies to GCSTWStart and GCSTWDone 66 | - The second GCSTWDone can happen after GCDone 67 | 68 | GC can get started by normal user goroutines, when they allocate and detect that the GC trigger condition has been 69 | met. The lucky goroutine will be responsible for running the runtime code that stops the world, sets up write 70 | barriers and so on. It returns to the user's code once the first STW phase is over and the concurrent mark phase has 71 | started. Since multiple goroutines may allocate in parallel, multiple goroutines might detect the GC trigger and 72 | attempt to start GC. All but one will block trying to acquire the semaphore, then not start GC once they unblock. 73 | 74 | The GCStart and the first pair of STWStart + STWStop will be sent from that goroutine, but note that these events 75 | form a timeline separate from normal goroutine events. In particular, seeing STWStart on the goroutine doesn't mean 76 | that its previous GoStart has been superseded, and we'll not see another GoStart after we see STWStop. 77 | 78 | The second STW phase and GCDone are sent from another goroutine, probably the background mark worker that determined 79 | that we're done. 80 | -------------------------------------------------------------------------------- /cmd/gotraceui/argminmax.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func argminmax[T ~uint64 | ~uint32 | ~uint16 | ~uint8](values []T) (minIdx, maxIdx int) { 4 | // OPT dispatch to SIMD where beneficial 5 | 6 | if len(values) == 0 { 7 | return -1, -1 8 | } 9 | min, max := values[0], values[0] 10 | for i, v := range values { 11 | if v < min { 12 | min = v 13 | minIdx = i 14 | } 15 | if v > max { 16 | max = v 17 | maxIdx = i 18 | } 19 | } 20 | 21 | return minIdx, maxIdx 22 | } 23 | -------------------------------------------------------------------------------- /cmd/gotraceui/assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "embed" 5 | "errors" 6 | "fmt" 7 | "image" 8 | "image/gif" 9 | "image/png" 10 | "io" 11 | "io/fs" 12 | "log" 13 | "math" 14 | "sync" 15 | 16 | "honnef.co/go/gotraceui/widget" 17 | 18 | "gioui.org/layout" 19 | "gioui.org/op/paint" 20 | "golang.org/x/image/draw" 21 | ) 22 | 23 | //go:embed data 24 | var data embed.FS 25 | 26 | var images sync.Map 27 | var animations sync.Map 28 | 29 | type imageKey struct { 30 | name string 31 | size int 32 | } 33 | 34 | func loadGIF(gtx layout.Context, name string, size int) (*gif.GIF, error) { 35 | pxPerDp := int(math.Round(float64(gtx.Metric.PxPerDp))) 36 | 37 | var rc io.ReadCloser 38 | var err error 39 | if size == 0 { 40 | rc, err = data.Open(fmt.Sprintf("data/%s-%dx.gif", name, pxPerDp)) 41 | } else { 42 | rc, err = data.Open(fmt.Sprintf("data/%s-%d.gif", name, size*pxPerDp)) 43 | } 44 | 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | defer rc.Close() 50 | 51 | img, err := gif.DecodeAll(rc) 52 | if err != nil { 53 | panic(fmt.Sprintf("couldn't load asset: %s", err)) 54 | } 55 | 56 | return img, nil 57 | } 58 | 59 | func loadImage(gtx layout.Context, name string, size int) (image.Image, error) { 60 | pxPerDp := int(math.Round(float64(gtx.Metric.PxPerDp))) 61 | 62 | var rc io.ReadCloser 63 | var err error 64 | if size == 0 { 65 | rc, err = data.Open(fmt.Sprintf("data/%s-%dx.png", name, pxPerDp)) 66 | } else { 67 | rc, err = data.Open(fmt.Sprintf("data/%s-%d.png", name, size*pxPerDp)) 68 | } 69 | 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | defer rc.Close() 75 | 76 | img, err := png.Decode(rc) 77 | if err != nil { 78 | panic(fmt.Sprintf("couldn't load asset: %s", err)) 79 | } 80 | 81 | return img, nil 82 | } 83 | 84 | func Image(gtx layout.Context, name string, size int) paint.ImageOp { 85 | pxPerDp := int(math.Round(float64(gtx.Metric.PxPerDp))) 86 | key := imageKey{name: name, size: size * pxPerDp} 87 | if op, ok := images.Load(key); ok { 88 | return op.(paint.ImageOp) 89 | } 90 | 91 | img, err := loadImage(gtx, name, size) 92 | if err != nil { 93 | if errors.Is(err, fs.ErrNotExist) && pxPerDp != 1 { 94 | // The image doesn't exist at the scale we need. Load the unscaled size and scale it at runtime. 95 | gtx := gtx 96 | gtx.Metric.PxPerDp = 1 97 | img, err = loadImage(gtx, name, size) 98 | if err != nil { 99 | panic(fmt.Sprintf("couldn't load asset: %s", err)) 100 | } 101 | 102 | log.Printf("asset %q missing at size %d and scale %d, scaling base version", name, size, pxPerDp) 103 | newBounds := img.Bounds() 104 | newBounds.Max.X *= pxPerDp 105 | newBounds.Max.Y *= pxPerDp 106 | dst := image.NewRGBA(newBounds) 107 | draw.NearestNeighbor.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Src, nil) 108 | 109 | img = dst 110 | } else { 111 | panic(fmt.Sprintf("couldn't load asset: %s", err)) 112 | } 113 | } 114 | 115 | op := paint.NewImageOp(img) 116 | // It's fine for this to be racy, worst case we do unnecessary work loading the same asset multiple times. 117 | images.Store(key, op) 118 | 119 | return op 120 | } 121 | 122 | func Animation(gtx layout.Context, name string, size int) *widget.GIF { 123 | pxPerDp := int(math.Round(float64(gtx.Metric.PxPerDp))) 124 | key := imageKey{name: name, size: size * pxPerDp} 125 | if op, ok := animations.Load(key); ok { 126 | return op.(*widget.GIF) 127 | } 128 | 129 | g, err := loadGIF(gtx, name, size) 130 | if err != nil { 131 | if errors.Is(err, fs.ErrNotExist) && pxPerDp != 1 { 132 | // The image doesn't exist at the scale we need. Load the unscaled size and scale it at runtime. 133 | gtx := gtx 134 | gtx.Metric.PxPerDp = 1 135 | g, err = loadGIF(gtx, name, size) 136 | if err != nil { 137 | panic(fmt.Sprintf("couldn't load asset: %s", err)) 138 | } 139 | 140 | log.Printf("asset %q missing at size %d and scale %d, scaling base version", name, size, pxPerDp) 141 | for i, img := range g.Image { 142 | newBounds := img.Bounds() 143 | newBounds.Max.X *= pxPerDp 144 | newBounds.Max.Y *= pxPerDp 145 | dst := image.NewPaletted(newBounds, img.Palette) 146 | draw.NearestNeighbor.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Src, nil) 147 | 148 | g.Image[i] = dst 149 | } 150 | g.Config.Width *= pxPerDp 151 | g.Config.Height *= pxPerDp 152 | } else { 153 | panic(fmt.Sprintf("couldn't load asset: %s", err)) 154 | } 155 | } 156 | 157 | op := widget.NewGIF(g) 158 | // It's fine for this to be racy, worst case we do unnecessary work loading the same asset multiple times. 159 | animations.Store(key, op) 160 | 161 | return op 162 | } 163 | -------------------------------------------------------------------------------- /cmd/gotraceui/assets/data/dance-128.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/cmd/gotraceui/assets/data/dance-128.gif -------------------------------------------------------------------------------- /cmd/gotraceui/assets/data/dance-32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/cmd/gotraceui/assets/data/dance-32.gif -------------------------------------------------------------------------------- /cmd/gotraceui/assets/data/dance-64.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/cmd/gotraceui/assets/data/dance-64.gif -------------------------------------------------------------------------------- /cmd/gotraceui/assets/data/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/cmd/gotraceui/assets/data/logo-128.png -------------------------------------------------------------------------------- /cmd/gotraceui/assets/data/logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/cmd/gotraceui/assets/data/logo-256.png -------------------------------------------------------------------------------- /cmd/gotraceui/colors.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "honnef.co/go/gotraceui/color" 5 | "honnef.co/go/gotraceui/trace/ptrace" 6 | ) 7 | 8 | const ( 9 | colorsLightBase = 58.51 10 | colorsChromaBase = 0.122 11 | colorLightStep1 = 15 12 | colorLightStep2 = 10 13 | ) 14 | 15 | var colors = [colorLast]color.Oklch{ 16 | colorStateUndetermined: oklch(colorsLightBase+colorLightStep1, colorsChromaBase, 70.54), // Manually chosen 17 | 18 | colorStateActive: oklch(colorsLightBase, colorsChromaBase, 143.74), // Manually chosen 19 | colorStateProcRunningNoG: oklch(colorsLightBase, colorsChromaBase, 206.35), // Manually chosen, same as colorStateReady 20 | colorStateProcRunningBlocked: oklch(colorsLightBase-5, colorsChromaBase, 23.89), // Manually chosen, same as colorStateBlocked 21 | colorStateProcRunningG: oklch(colorsLightBase, colorsChromaBase, 143.74), // Manually chosen, same as colorStateActive 22 | colorStateStack: oklchDelta(oklch(colorsLightBase, colorsChromaBase, 143.74), colorLightStep1, -0.01, 0), 23 | colorStateCPUSample: oklchDelta(oklchDelta(oklch(colorsLightBase, colorsChromaBase, 143.74), colorLightStep1, -0.01, 0), colorLightStep2, -0.01, 0), 24 | 25 | colorStateReady: oklch(colorsLightBase, colorsChromaBase, 206.35), // Manually chosen 26 | colorStateWaitingPreempted: oklch(colorsLightBase, colorsChromaBase, 206.35), // Manually chosen 27 | colorStateInactive: oklch(colorsLightBase, 0, 0), 28 | 29 | colorStateUserRegion: oklch(colorsLightBase+colorLightStep1+colorLightStep2, colorsChromaBase, 331.18), // Manually chosen 30 | 31 | // Manually chosen. This is the rarest blocked state, so we darken it to have more range for the other states. 32 | colorStateBlocked: oklch(colorsLightBase-5, colorsChromaBase, 23.89), 33 | colorStateBlockedSyscall: oklch(colorsLightBase, colorsChromaBase, 23.89), 34 | colorStateBlockedNet: oklch(colorsLightBase+6, colorsChromaBase-0.01, 23.89), 35 | colorStateBlockedHappensBefore: oklch(colorsLightBase+colorLightStep2, colorsChromaBase, 23.89), 36 | colorStateBlockedGC: oklch(colorsLightBase, colorsChromaBase, 0), // a blend of colorStateGC and red 37 | 38 | colorStateGC: oklch(colorsLightBase, colorsChromaBase, 302.36), 39 | colorStateSTW: oklch(colorsLightBase, colorsChromaBase+0.072, 23.89), // STW is the most severe form of blocking, hence the increased chroma 40 | 41 | colorTimelineLabel: oklch(62.68, 0, 0), 42 | colorTimelineBorder: oklch(89.75, 0, 0), 43 | 44 | // // TODO(dh): find a nice color for this 45 | // We don't use the l constant for thse colors because they're independent from the span colors 46 | colorSpanHighlightedPrimaryOutline: oklch(70.71, 0.322, 328.36), 47 | colorSpanHighlightedSecondaryOutline: oklch(88.44, 0.27, 137.68), 48 | 49 | colorStateMerged: oklch(colorsLightBase+colorLightStep1, colorsChromaBase, 109.91), // Manually chosen, made brighter so it stands out in gradients 50 | 51 | colorStateStuck: oklch(0, 0, 0), 52 | colorStateDone: oklch(0, 0, 0), 53 | colorEvent: oklch(colorsLightBase, colorsChromaBase, 0), 54 | colorMergedEvents: oklch(colorsLightBase+colorLightStep1, colorsChromaBase, 284.44), 55 | 56 | colorStateUnknown: oklch(96.8, 0.211, 109.77), 57 | colorStatePlaceholderStackSpan: oklch(92.59, 0.025, 106.88), 58 | } 59 | 60 | var mappedColors [len(colors)]color.LinearSRGB 61 | 62 | func init() { 63 | for i, c := range colors { 64 | mappedColors[i] = c.MapToSRGBGamut() 65 | } 66 | } 67 | 68 | type colorIndex uint8 69 | 70 | const ( 71 | colorStateUnknown colorIndex = iota 72 | 73 | colorStateUndetermined 74 | 75 | colorStateInactive 76 | colorStateActive 77 | 78 | colorStateBlocked 79 | colorStateBlockedHappensBefore 80 | colorStateBlockedNet 81 | colorStateBlockedGC 82 | colorStateBlockedSyscall 83 | colorStateGC 84 | colorStateSTW 85 | 86 | colorStateReady 87 | colorStateWaitingPreempted 88 | colorStateStuck 89 | colorStateMerged 90 | colorStateUserRegion 91 | colorStateStack 92 | colorStateCPUSample 93 | colorStateDone 94 | colorStatePlaceholderStackSpan 95 | 96 | colorStateProcRunningG 97 | colorStateProcRunningNoG 98 | colorStateProcRunningBlocked 99 | 100 | colorStateLast 101 | 102 | colorTimelineLabel 103 | colorTimelineBorder 104 | 105 | colorSpanHighlightedPrimaryOutline 106 | colorSpanHighlightedSecondaryOutline 107 | 108 | colorEvent 109 | colorMergedEvents 110 | 111 | colorLast 112 | ) 113 | 114 | var stateColors = [256]colorIndex{ 115 | ptrace.StateUndetermined: colorStateUndetermined, 116 | 117 | // per-G states 118 | ptrace.StateInactive: colorStateInactive, 119 | ptrace.StateActive: colorStateActive, 120 | ptrace.StateBlocked: colorStateBlocked, 121 | ptrace.StateBlockedSend: colorStateBlockedHappensBefore, 122 | ptrace.StateBlockedRecv: colorStateBlockedHappensBefore, 123 | ptrace.StateBlockedSelect: colorStateBlockedHappensBefore, 124 | ptrace.StateBlockedSync: colorStateBlockedHappensBefore, 125 | ptrace.StateBlockedCond: colorStateBlockedHappensBefore, 126 | ptrace.StateBlockedNet: colorStateBlockedNet, 127 | ptrace.StateBlockedGC: colorStateBlockedGC, 128 | ptrace.StateBlockedSyscall: colorStateBlockedSyscall, 129 | ptrace.StateStuck: colorStateStuck, 130 | ptrace.StateReady: colorStateReady, 131 | ptrace.StateWaitingPreempted: colorStateWaitingPreempted, 132 | ptrace.StateCreated: colorStateReady, 133 | ptrace.StateGCMarkAssist: colorStateGC, 134 | ptrace.StateGCSweep: colorStateGC, 135 | ptrace.StateGCIdle: colorStateGC, 136 | ptrace.StateGCDedicated: colorStateGC, 137 | ptrace.StateGCFractional: colorStateGC, 138 | ptrace.StateBlockedSyncOnce: colorStateBlockedHappensBefore, 139 | ptrace.StateBlockedSyncTriggeringGC: colorStateGC, 140 | ptrace.StateUserRegion: colorStateUserRegion, 141 | ptrace.StateTask: colorStateUserRegion, 142 | ptrace.StateStack: colorStateStack, 143 | ptrace.StateCPUSample: colorStateCPUSample, 144 | 145 | // per-P states 146 | ptrace.StateProcRunningNoG: colorStateProcRunningNoG, 147 | ptrace.StateProcRunningG: colorStateProcRunningG, 148 | ptrace.StateProcRunningBlocked: colorStateProcRunningBlocked, 149 | } 150 | 151 | // oklch specifies a color in Oklch. 152 | // 100 >= l >= 0 153 | // 0.37 >= c >= 0 154 | // 360 > h >= 0 155 | func oklch(l, c, h float32) color.Oklch { 156 | return color.Oklch{L: l / 100, C: c, H: h, A: 1} 157 | } 158 | 159 | func oklcha(l, c, h, a float32) color.Oklch { 160 | return color.Oklch{L: l / 100, C: c, H: h, A: a} 161 | } 162 | 163 | func oklchDelta(b color.Oklch, l, c, h float32) color.Oklch { 164 | b.L += l / 100 165 | b.C += c 166 | b.H += h 167 | if b.L < 0 { 168 | b.L = 0 169 | } 170 | if b.L > 1 { 171 | b.L = 1 172 | } 173 | if b.C < 0 { 174 | b.C = 0 175 | } 176 | return b 177 | } 178 | -------------------------------------------------------------------------------- /cmd/gotraceui/debug_shared.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "runtime/pprof" 9 | "sync" 10 | "time" 11 | 12 | "honnef.co/go/gotraceui/color" 13 | ) 14 | 15 | var ( 16 | errExitAfterParsing = errors.New("we were instructed to exit after parsing") 17 | errExitAfterLoading = errors.New("we were instructed to exit after loading") 18 | ) 19 | 20 | type debugGraph struct { 21 | title string 22 | width time.Duration 23 | background color.Oklch 24 | fixedZero bool 25 | stickyLastValue bool 26 | 27 | mu sync.Mutex 28 | values []struct { 29 | when time.Time 30 | val float64 31 | } 32 | } 33 | 34 | type DebugWindow struct { 35 | cvStart debugGraph 36 | cvEnd debugGraph 37 | cvY debugGraph 38 | cvPxPerNs debugGraph 39 | animationProgress debugGraph 40 | animationRatio debugGraph 41 | frametimes debugGraph 42 | } 43 | 44 | func writeMemprofile(s string) { 45 | f, err := os.Create(s) 46 | if err != nil { 47 | fmt.Fprintln(os.Stderr, "couldn't write memory profile:", err) 48 | return 49 | } 50 | defer f.Close() 51 | runtime.GC() 52 | if err := pprof.WriteHeapProfile(f); err != nil { 53 | fmt.Fprintln(os.Stderr, "couldn't write memory profile:", err) 54 | } 55 | } 56 | 57 | func assert(b bool, msg string) { 58 | if !b { 59 | panic(fmt.Sprintf("failed assertion: %s", msg)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /cmd/gotraceui/debug_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !debug 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "time" 8 | 9 | "gioui.org/app" 10 | ) 11 | 12 | const debug = false 13 | 14 | func (g *debugGraph) addValue(ts time.Time, val float64) {} 15 | 16 | func NewDebugWindow() *DebugWindow { return &DebugWindow{} } 17 | func (dwin *DebugWindow) Run(win *app.Window) error { return errors.New("debugging disabled") } 18 | -------------------------------------------------------------------------------- /cmd/gotraceui/float32.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func round32(f float32) float32 { 8 | return float32(math.Round(float64(f))) 9 | } 10 | -------------------------------------------------------------------------------- /cmd/gotraceui/gc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | rdebug "runtime/debug" 6 | "runtime/metrics" 7 | rtrace "runtime/trace" 8 | "time" 9 | ) 10 | 11 | // GCScheduler monitors memory usage and manipulates GOGC to enforce an absolute limit on the amount of 12 | // possible garbage. 13 | type GCScheduler struct { 14 | // How much additional memory can be allocated before GC has to run. Defaults to 1 GiB. 15 | Overhead int 16 | // Don't manage GOGC until live memory exceeds this threshold. Defaults to Overhead, set to positive 17 | // non-zero value to override. 18 | Threshold int 19 | pause, resume chan struct{} 20 | } 21 | 22 | func NewGCScheduler(overhead, threshold int) *GCScheduler { 23 | return &GCScheduler{ 24 | Overhead: overhead, 25 | Threshold: threshold, 26 | pause: make(chan struct{}), 27 | resume: make(chan struct{}), 28 | } 29 | } 30 | 31 | func (gc *GCScheduler) Pause() { 32 | gc.pause <- struct{}{} 33 | } 34 | 35 | func (gc *GCScheduler) Resume() { 36 | gc.resume <- struct{}{} 37 | } 38 | 39 | func (gc *GCScheduler) Run() { 40 | // Collecting /gc/heap metrics is cheaper than calling runtime.ReadMemStats, but it isn't free. It still 41 | // involves locking, disabling preemption, and waiting on writers. Don't do it too often. 42 | const interval = 1 * time.Second 43 | t := time.NewTicker(interval) 44 | defer t.Stop() 45 | 46 | // Get original value of GOGC, which we restore when pausing. There is no way to get the current GOGC 47 | // value without setting a new one. We temporarily set it to 100 because that's most likely the GOGC we're 48 | // already running with. This is nicer than setting it to -1, as GOGC=off sets a heap target of MaxUint64, 49 | // which makes looking at traces of Gotraceui slightly more annoying. 50 | origGOGC := rdebug.SetGCPercent(-1) 51 | prevGOGC := origGOGC 52 | rdebug.SetGCPercent(origGOGC) 53 | 54 | // The sum of these 3 metrics matches what the Go runtime uses to set the GC goal (as per 55 | // $GOROOT/src/runtime/mgcpacer.go.) 56 | samples := []metrics.Sample{ 57 | { 58 | // The size of the heap, as per the last GC cycle 59 | Name: "/gc/heap/live:bytes", 60 | }, 61 | { 62 | // The size of scannable stacks 63 | Name: "/gc/scan/stack:bytes", 64 | }, 65 | { 66 | // The size of scannable globals 67 | Name: "/gc/scan/globals:bytes", 68 | }, 69 | } 70 | var overhead, threshold uint64 71 | if gc.Overhead <= 0 { 72 | overhead = 1024 * 1024 * 1024 // 1 GiB 73 | } else { 74 | overhead = uint64(gc.Overhead) 75 | } 76 | if gc.Threshold <= 0 { 77 | threshold = overhead 78 | } else { 79 | threshold = uint64(gc.Threshold) 80 | } 81 | for { 82 | select { 83 | case <-t.C: 84 | metrics.Read(samples) 85 | if samples[0].Value.Kind() == metrics.KindBad { 86 | // This version of Go doesn't support the requested metric. Stop the timer, but don't return, 87 | // so that Pause and Resume continue to work, albeit as (mostly) no-ops. 88 | rtrace.Logf(context.Background(), "GCScheduler", "missing metric, turning on Go's pacer") 89 | t.Stop() 90 | rdebug.SetGCPercent(origGOGC) 91 | continue 92 | } 93 | var live uint64 94 | for _, s := range samples { 95 | if s.Value.Kind() == metrics.KindBad { 96 | // We don't care if we couldn't get the size of stacks or globals, and we've already 97 | // validated the heap metric. 98 | continue 99 | } 100 | live += s.Value.Uint64() 101 | } 102 | 103 | var newGOGC int 104 | if live < threshold { 105 | // Guard against computing invalid GOGC (when live == 0). Also default to normal GC pacing if 106 | // the live memory is smaller than the threshold. 107 | rtrace.Logf(context.Background(), "GCScheduler", "live %d < threshold %d, setting GOGC=100", live, threshold) 108 | newGOGC = 100 109 | } else { 110 | newGOGC = int(100 * (float64(live+overhead)/float64(live) - 1)) 111 | rtrace.Logf(context.Background(), "GCScheduler", "live %d, overhead %d, setting GOGC=%d", live, overhead, newGOGC) 112 | } 113 | // Don't call SetGCPercent unnecessarily. It has to run on the system stack and hold a lock. 114 | if newGOGC != prevGOGC { 115 | rtrace.Logf(context.Background(), "GCScheduler", "prevGOGC=%d != newGOGC=%d, updating GOGC", prevGOGC, newGOGC) 116 | rdebug.SetGCPercent(newGOGC) 117 | } 118 | 119 | prevGOGC = newGOGC 120 | case <-gc.pause: 121 | rtrace.Logf(context.Background(), "GCScheduler", "pausing scheduler, restoring original GOGC %d", origGOGC) 122 | t.Stop() 123 | rdebug.SetGCPercent(origGOGC) 124 | case <-gc.resume: 125 | rtrace.Logf(context.Background(), "GCScheduler", "resuming scheduler") 126 | t.Reset(interval) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /cmd/gotraceui/labels.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "honnef.co/go/gotraceui/trace/ptrace" 4 | 5 | // Mapping from states to span labels 6 | var spanStateLabels = [...][]string{ 7 | ptrace.StateUndetermined: {"undetermined"}, 8 | ptrace.StateInactive: {"inactive"}, 9 | ptrace.StateActive: {"active"}, 10 | ptrace.StateGCIdle: {"GC (idle)", "I"}, 11 | ptrace.StateGCDedicated: {"GC (dedicated)", "D"}, 12 | ptrace.StateGCFractional: {"GC (fractional)", "F"}, 13 | ptrace.StateBlocked: {"blocked"}, 14 | ptrace.StateBlockedSend: {"send"}, 15 | ptrace.StateBlockedRecv: {"recv"}, 16 | ptrace.StateBlockedSelect: {"select"}, 17 | ptrace.StateBlockedSync: {"sync"}, 18 | ptrace.StateBlockedSyncOnce: {"sync.Once"}, 19 | ptrace.StateBlockedSyncTriggeringGC: {"triggering GC", "T"}, 20 | ptrace.StateBlockedCond: {"sync.Cond"}, 21 | ptrace.StateBlockedNet: {"I/O"}, 22 | ptrace.StateBlockedGC: {"GC assist wait", "W"}, 23 | ptrace.StateBlockedSyscall: {"syscall"}, 24 | ptrace.StateStuck: {"stuck"}, 25 | ptrace.StateReady: {"ready"}, 26 | ptrace.StateWaitingPreempted: {"preempted"}, 27 | ptrace.StateCreated: {"created"}, 28 | ptrace.StateGCMarkAssist: {"GC mark assist", "M"}, 29 | ptrace.StateGCSweep: {"GC sweep", "S"}, 30 | ptrace.StateProcRunningNoG: {"no goroutine"}, 31 | ptrace.StateLast: nil, 32 | } 33 | 34 | // Mapping from states to tooltips 35 | var tooltipStateLabels = [ptrace.StateLast]string{ 36 | ptrace.StateUndetermined: "State: undetermined", 37 | ptrace.StateInactive: "State: inactive", 38 | ptrace.StateActive: "State: active", 39 | ptrace.StateGCDedicated: "State: GC (dedicated)", 40 | ptrace.StateGCFractional: "State: GC (fractional)", 41 | ptrace.StateGCIdle: "State: GC (idle)", 42 | ptrace.StateBlocked: "State: blocked", 43 | ptrace.StateBlockedSend: "State: blocked on channel send", 44 | ptrace.StateBlockedRecv: "State: blocked on channel recv", 45 | ptrace.StateBlockedSelect: "State: blocked on select", 46 | ptrace.StateBlockedSync: "State: blocked on mutex", 47 | ptrace.StateBlockedSyncOnce: "State: blocked on sync.Once", 48 | ptrace.StateBlockedSyncTriggeringGC: "State: blocked triggering GC", 49 | ptrace.StateBlockedCond: "State: blocked on condition variable", 50 | ptrace.StateBlockedNet: "State: blocked on polled I/O", 51 | ptrace.StateBlockedGC: "State: GC assist wait", 52 | ptrace.StateBlockedSyscall: "State: blocked on syscall", 53 | ptrace.StateStuck: "State: stuck", 54 | ptrace.StateReady: "State: ready", 55 | ptrace.StateWaitingPreempted: "State: preempted", 56 | ptrace.StateCreated: "State: ready", 57 | ptrace.StateGCMarkAssist: "State: GC mark assist", 58 | ptrace.StateGCSweep: "State: GC sweep", 59 | 60 | ptrace.StateProcRunningNoG: "State: running without goroutine", 61 | ptrace.StateProcRunningG: "State: running with goroutine", 62 | ptrace.StateProcRunningBlocked: "State: running with blocked goroutine", 63 | } 64 | 65 | // Mapping from states to their names as used by statistics, filters, etc. 66 | var stateNames = [ptrace.StateLast]string{ 67 | ptrace.StateUndetermined: "undetermined", 68 | ptrace.StateInactive: "inactive", 69 | ptrace.StateActive: "active", 70 | ptrace.StateGCIdle: "GC (idle)", 71 | ptrace.StateGCDedicated: "GC (dedicated)", 72 | ptrace.StateGCFractional: "GC (fractional)", 73 | ptrace.StateBlocked: "blocked (other)", 74 | ptrace.StateBlockedSend: "blocked (channel send)", 75 | ptrace.StateBlockedRecv: "blocked (channel receive)", 76 | ptrace.StateBlockedSelect: "blocked (select)", 77 | ptrace.StateBlockedSync: "blocked (sync)", 78 | ptrace.StateBlockedSyncOnce: "blocked (sync.Once)", 79 | ptrace.StateBlockedSyncTriggeringGC: "blocked (triggering GC)", 80 | ptrace.StateBlockedCond: "blocked (sync.Cond)", 81 | ptrace.StateBlockedNet: "blocked (pollable I/O)", 82 | ptrace.StateBlockedGC: "blocked (GC)", 83 | ptrace.StateBlockedSyscall: "blocked (syscall)", 84 | ptrace.StateStuck: "stuck", 85 | ptrace.StateReady: "ready", 86 | ptrace.StateWaitingPreempted: "preempted", 87 | ptrace.StateCreated: "created", 88 | ptrace.StateGCMarkAssist: "GC (mark assist)", 89 | ptrace.StateGCSweep: "GC (sweep assist)", 90 | ptrace.StateUserRegion: "user region", 91 | ptrace.StateTask: "task", 92 | ptrace.StateStack: "stack frame", 93 | ptrace.StateProcRunningNoG: "proc running without goroutine", 94 | ptrace.StateProcRunningG: "proc running", 95 | ptrace.StateProcRunningBlocked: "proc waiting for goroutine to unblock", 96 | } 97 | 98 | // Mapping from states to their names as used by statistics, filters, etc. 99 | var stateNamesCapitalized = [ptrace.StateLast]string{ 100 | ptrace.StateUndetermined: "Undetermined", 101 | ptrace.StateInactive: "Inactive", 102 | ptrace.StateActive: "Active", 103 | ptrace.StateGCIdle: "GC (idle)", 104 | ptrace.StateGCDedicated: "GC (dedicated)", 105 | ptrace.StateGCFractional: "GC (fractional)", 106 | ptrace.StateBlocked: "Blocked (other)", 107 | ptrace.StateBlockedSend: "Blocked (channel send)", 108 | ptrace.StateBlockedRecv: "Blocked (channel receive)", 109 | ptrace.StateBlockedSelect: "Blocked (select)", 110 | ptrace.StateBlockedSync: "Blocked (sync)", 111 | ptrace.StateBlockedSyncOnce: "Blocked (sync.Once)", 112 | ptrace.StateBlockedSyncTriggeringGC: "Blocked (triggering GC)", 113 | ptrace.StateBlockedCond: "Blocked (sync.Cond)", 114 | ptrace.StateBlockedNet: "Blocked (pollable I/O)", 115 | ptrace.StateBlockedGC: "Blocked (GC)", 116 | ptrace.StateBlockedSyscall: "Blocked (syscall)", 117 | ptrace.StateStuck: "Stuck", 118 | ptrace.StateReady: "Ready", 119 | ptrace.StateWaitingPreempted: "Preempted", 120 | ptrace.StateCreated: "Created", 121 | ptrace.StateGCMarkAssist: "GC (mark assist)", 122 | ptrace.StateGCSweep: "GC (sweep assist)", 123 | ptrace.StateUserRegion: "User region", 124 | ptrace.StateTask: "Task", 125 | ptrace.StateStack: "Stack frame", 126 | ptrace.StateCPUSample: "Stack frame (sampled)", 127 | ptrace.StateProcRunningNoG: "Proc running without goroutine", 128 | ptrace.StateProcRunningG: "Proc running", 129 | ptrace.StateProcRunningBlocked: "Proc waiting for goroutine to unblock", 130 | } 131 | -------------------------------------------------------------------------------- /cmd/gotraceui/processor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | rtrace "runtime/trace" 6 | "time" 7 | 8 | "honnef.co/go/gotraceui/layout" 9 | "honnef.co/go/gotraceui/theme" 10 | "honnef.co/go/gotraceui/trace/ptrace" 11 | ) 12 | 13 | type ProcessorTooltip struct { 14 | p *ptrace.Processor 15 | trace *Trace 16 | } 17 | 18 | func (tt ProcessorTooltip) Layout(win *theme.Window, gtx layout.Context) layout.Dimensions { 19 | defer rtrace.StartRegion(context.Background(), "main.ProcessorTooltip.Layout").End() 20 | 21 | // OPT(dh): compute statistics once, not on every frame 22 | 23 | tr := tt.trace 24 | // FIXME(dh): this doesn't seem right for processors that didn't start at 0 25 | d := time.Duration(tr.End()) 26 | 27 | var userD, gcD time.Duration 28 | for i := range tt.p.Spans { 29 | s := &tt.p.Spans[i] 30 | d := s.Duration() 31 | 32 | _ = d 33 | 34 | ev := tr.Event(s.StartEvent) 35 | _ = ev 36 | // switch ev.Type { 37 | // case trace.EvGoStart: 38 | // userD += d 39 | // case trace.EvGoStartLabel: 40 | // gcD += d 41 | // default: 42 | // panic(fmt.Sprintf("unexepcted event type %d", ev.Type)) 43 | // } 44 | } 45 | 46 | userPct := float32(userD) / float32(d) * 100 47 | gcPct := float32(gcD) / float32(d) * 100 48 | inactiveD := d - userD - gcD 49 | inactivePct := float32(inactiveD) / float32(d) * 100 50 | 51 | l := local.Sprintf( 52 | "Processor %[1]d\n"+ 53 | "Spans: %[2]d\n"+ 54 | "Time running user code: %[3]s (%.2[4]f%%)\n"+ 55 | "Time running GC workers: %[5]s (%.2[6]f%%)\n"+ 56 | "Time inactive: %[7]s (%.2[8]f%%)", 57 | tt.p.ID, 58 | len(tt.p.Spans), 59 | roundDuration(userD), userPct, 60 | roundDuration(gcD), gcPct, 61 | roundDuration(inactiveD), inactivePct, 62 | ) 63 | 64 | return theme.Tooltip(win.Theme, l).Layout(win, gtx) 65 | } 66 | 67 | func processorTrackSpanTooltip(win *theme.Window, gtx layout.Context, tr *Trace, spans Items[ptrace.Span]) layout.Dimensions { 68 | var label string 69 | if spans.Len() == 1 { 70 | s := spans.AtPtr(0) 71 | ev := tr.Event(s.StartEvent) 72 | label = tooltipStateLabels[s.State] + "\n" 73 | switch s.State { 74 | case ptrace.StateProcRunningG, ptrace.StateProcRunningBlocked: 75 | g := ev.StateTransition().Resource.Goroutine() 76 | // OPT(dh): cache these strings 77 | label += local.Sprintf("Goroutine %d: %s\n", g, tr.G(g).Function) 78 | } 79 | } else { 80 | label = local.Sprintf("%d spans\n", spans.Len()) 81 | } 82 | label += spansDurationForTooltip(spans) 83 | return theme.Tooltip(win.Theme, label).Layout(win, gtx) 84 | } 85 | 86 | func processorTrackSpanLabel(spans Items[ptrace.Span], tr *Trace, out []string) []string { 87 | if spans.Len() != 1 { 88 | return out 89 | } 90 | var labels []string 91 | s := spans.AtPtr(0) 92 | switch s.State { 93 | case ptrace.StateProcRunningG, ptrace.StateProcRunningBlocked: 94 | g := tr.G(tr.Event(spans.AtPtr(0).StartEvent).StateTransition().Resource.Goroutine()) 95 | labels = tr.goroutineSpanLabels(g) 96 | default: 97 | labels = spanStateLabels[s.State] 98 | } 99 | out = append(out, labels...) 100 | return out 101 | } 102 | 103 | func processorTrackSpanColor(span *ptrace.Span, tr *Trace) (out colorIndex) { 104 | if span.Tags&ptrace.SpanTagGC != 0 && span.State == ptrace.StateProcRunningG { 105 | return colorStateGC 106 | } else { 107 | // TODO(dh): support goroutines that are currently doing GC assist work. this would require splitting spans, however. 108 | return stateColors[span.State] 109 | } 110 | } 111 | 112 | func processorTrackSpanContextMenu(spans Items[ptrace.Span], cv *Canvas) []*theme.MenuItem { 113 | items := []*theme.MenuItem{ 114 | newZoomMenuItem(cv, spans), 115 | newOpenSpansMenuItem(spans), 116 | } 117 | 118 | if spans.Len() == 1 { 119 | gid := cv.trace.Event(spans.AtPtr(0).StartEvent).StateTransition().Resource.Goroutine() 120 | items = append(items, &theme.MenuItem{ 121 | Label: PlainLabel(local.Sprintf("Scroll to goroutine %d", gid)), 122 | Action: func() theme.Action { 123 | return &ScrollToObjectAction{Object: cv.trace.G(gid)} 124 | }, 125 | }) 126 | } 127 | 128 | return items 129 | } 130 | 131 | func NewProcessorTimeline(tr *Trace, cv *Canvas, p *ptrace.Processor) *Timeline { 132 | l := local.Sprintf("Processor %d", p.ID) 133 | tl := &Timeline{ 134 | cv: cv, 135 | 136 | widgetTooltip: func(win *theme.Window, gtx layout.Context, tl *Timeline) layout.Dimensions { 137 | return ProcessorTooltip{p, cv.trace}.Layout(win, gtx) 138 | }, 139 | item: p, 140 | label: l, 141 | shortName: l, 142 | } 143 | tl.tracks = []*Track{ 144 | NewTrack(tl, TrackKindUnspecified), 145 | } 146 | 147 | ss := SimpleItems[ptrace.Span, any]{ 148 | items: p.Spans, 149 | container: ItemContainer{ 150 | Timeline: tl, 151 | Track: tl.tracks[0], 152 | }, 153 | subslice: true, 154 | } 155 | tl.tracks[0].Start = p.Spans[0].Start 156 | tl.tracks[0].End = p.Spans[len(p.Spans)-1].End 157 | tl.tracks[0].spans = theme.Immediate[Items[ptrace.Span]](ss) 158 | tl.tracks[0].spanLabel = processorTrackSpanLabel 159 | tl.tracks[0].spanColor = processorTrackSpanColor 160 | tl.tracks[0].spanTooltip = processorTrackSpanTooltip 161 | tl.tracks[0].spanContextMenu = processorTrackSpanContextMenu 162 | 163 | addStackTracks(tl, p, tr) 164 | 165 | return tl 166 | } 167 | -------------------------------------------------------------------------------- /cmd/gotraceui/testdata/fuzz/FuzzLoadTrace/015cd8e72fab9610c5fac7a531c9d5fdb90e9847bae917d8632d81e180472b52: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("go 1.11 trace\x00\x00\x00A\x00\xeb\x9b\xee\x92\xd5\xf9\x91\x01\xcd\x04\x06\x01\x02\x01\xcd\x04}\x02\x03\x01_\x05\x02\xcd\x04\n\x03\x04\x01_\x03\x03\xcd\x04n\x04\x05\x01_\x02\x04\xcd\x04r\x05\x06\x01`\x03\x05E\a\x00f\t\x01%\x01\x0eGC (dedicated)%\x02\x0fGC (fractional)%\x03\tGC (idle)\x84\x9a\x01\f\a\xcd\x05\x97\x12\x06\b\ta\xd7\x06\x80\x80=ac\x80\xc0=a\xc8\x05\x80\xc0=a\v\x80\x80>ac\x80\x80>a\v\x80\xc0>a\x13\x80\xc0>a\n\x80\xc0@a>\x80\xc0@a\b\x80\x80Aa\x9e\x01\x80\x80Aa\t\x80\xc0Aa\xc0\x01\x80\xc0Aa\b\x80\x80Ba\xb9\x1e\x80\x80Ca\x0e\x80\xc0C\xcd\x05\x90\x1d\a\f\rV\xd6\x05\x0ef\x9e\x01\a\xa7\x1e\x01\x0f\x0f\xfe\x01f,\x01\\\x81\x01\x10\\\x9b\x05\x11Q\xde\x01\x12A\x01\xed\xa9\xee\x92\xd5\xf9\x91\x01E\a\x04\x06>E\xfe\f\x06\x8e\xa0\x19\x06\x01a\xa3\x01\x80\x80Ba+\x80\xc0Ba\xa4\x02\x80\xc0Ba\x11\x80\x80C\\\x8f\x01\nT\xe3\f\v\x06\xeb\x02E\xd8 \x06\x062E\x9e\x03\x06\x06+A\x02\xda\xd3\xee\x92\xd5\xf9\x91\x01E\x04\x04\x06U\x02\x95\xec\x83\x1aA\x00\x9f\xae\xef\x92\xd5\xf9\x91\x01%\x04\x16testing.runTests.func1%\x05=/usr/local/google/home/hakim/golang/go/src/testing/testing.go%\x06\x0ftesting.tRunner%\a\x10testing.runTests%\b\x10testing.(*M).Run%\t\x16net/http_test.TestMain%\n@/usr/local/google/home/hakim/golang/go/src/net/http/main_test.go%\v\tmain.main%\f\f_testmain.go\xc31\r\x06\x81\xfe\xbb\x02\x04\x05\xac\b\x8f\xff\xba\x02\x06\x05\x89\x06í\xbb\x02\a\x05\xa5\b\xa0\x8d\xbb\x02\b\x05\xd2\a\xca\xd2\xd3\x03\t\n\x17\xb0\xdf\xf9\x03\v\f\xee\a%\r\fruntime.main%\x0e:/usr/local/google/home/hakim/golang/go/src/runtime/proc.go\xc3\t\x02\x01\xb0\xc0\x8b\x02\r\x0em%\x0f\x11runtime.chansend1%\x10:/usr/local/google/home/hakim/golang/go/src/runtime/chan.go%\x11\x15testing.tRunner.func1\xc38\x0e\a䜁\x02\x0f\x10}\xe2\xf9\xbb\x02\x11\x05\x84\x06\xb1\xff\xba\x02\x06\x05\x8f\x06í\xbb\x02\a\x05\xa5\b\xa0\x8d\xbb\x02\b\x05\xd2\a\xca\xd2\xd3\x03\t\n\x17\xb0\xdf\xf9\x03\v\f\xee\a%\x12\x11runtime.ReadTrace%\x13;/usr/local/google/home/hakim/golang/go/src/runtime/trace.go%\x14\x19runtime/trace.Start.func1%\x15A/usr/local/google/home/hakim/golang/go/src/runtime/trace/trace.go\xc3\x12\v\x02Ŏ\x93\x02\x12\x13\x8b\x03\xb6\xa4\xb9\x02\x14\x15\x81\x01%\x16\x13runtime/trace.Start%\x17\x13testing.(*M).before\xc3(\t\x05\x99\xa2\xb9\x02\x16\x15\x7f\xb4\xb7\xbb\x02\x17\x05\xcd\bNj\xbb\x02\b\x05\xce\a\xca\xd2\xd3\x03\t\n\x17\xb0\xdf\xf9\x03\v\f\xee\a%\x18\x14runtime.traceGoSched%\x19\x11runtime.StopTrace%\x1a\x12runtime/trace.Stop%\x1b\x1atesting.(*M).writeProfiles%\x1c\x18testing.(*M).after.func1%\x1d\x0fsync.(*Once).Do%\x1e7/usr/local/google/home/hakim/golang/go/src/sync/once.go%\x1f\x12testing.(*M).after\xc3P\x12\n\x8aޓ\x02\x18\x13\xa4\b\xdd\xfe\x92\x02\x19\x13\x9f\x02ģ\xb9\x02\x1a\x15\x93\x01\xdfʻ\x02\x1b\x05\x8a\t\xc9\xfe\xbb\x02\x1c\x05\xf7\b\xad\xac\x9c\x02\x1d\x1e,˼\xbb\x02\x1f\x05\xf6\b\xf2\x91\xbb\x02\b\x05\xde\a\xca\xd2\xd3\x03\t\n\x17\xb0\xdf\xf9\x03\v\f\xee\a% \x11runtime.chanrecv1%!\x18testing.runTests.func1.1\xc3\x12\x0f\x02ڳ\x81\x02 \x10\x92\x03\xca\xfc\xbb\x02!\x05\xac\b%\"\x0fruntime.runfinq%#/usr/local/google/home/hakim/golang/go/src/runtime/mgcsweep.go\xc3\t\x04\x01\xf0\x82\x88\x0212.%3\vfmt.Println\xc3Q\x11\n\x9e\xc0\x9f\x02$%\xfe\a\xf8\x87\x9f\x02&'\xbf\x01\x91ڥ\x02()\x80\x02\xadا\x02*+\xf3\x01\x8e\x89\xa7\x02,-\x91\x01\xfa\x80\xb2\x02./\xff\x01Ƃ\xb2\x023/\x88\x02呻\x02\b\x05\xdd\a\xca\xd2\xd3\x03\t\n\x17\xb0\xdf\xf9\x03\v\f\xee\a\xc3\t\b\x01\xf0\xa3\xb9\x02\x14\x15\x7f\xc3\n\f\x01\x90\xfc\xbb\x02!\x05\xac\b%4\x15runtime.startTheWorld%5\x12runtime.StartTrace\xc38\a\a\x9c\x94\x8c\x024\x0e\xcd\a\xf4\xfc\x92\x025\x13\x8b\x02š\xb9\x02\x16\x15|\xb4\xb7\xbb\x02\x17\x05\xcd\bNj\xbb\x02\b\x05\xce\a\xca\xd2\xd3\x03\t\n\x17\xb0\xdf\xf9\x03\v\f\xee\a\xc3!\x01\x04\xb4\xb7\xbb\x02\x17\x05\xcd\bNj\xbb\x02\b\x05\xce\a\xca\xd2\xd3\x03\t\n\x17\xb0\xdf\xf9\x03\v\f\xee\a%6\x0eos/signal.loop%7C/usr/local/google/home/hakim/golang/go/src/os/signal/signal_unix.go\xc3\t\x06\x01\xb0\x9f\xc9\x0367\x14") -------------------------------------------------------------------------------- /cmd/gotraceui/testdata/fuzz/FuzzLoadTrace/048e23791869d19df7d253944aa22adc8e864f6420813afc377fbb50cf20a249: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("go 1.11 trace\x00\x00\x00A\x00\xf4\x8e\xff\x99\xd5\xf9\x91\x01\xcd\x04\x06\x01\x02\x01_\x0e\x01\xcd\x04t\x02\x03\x01_\x03\x02\xcd\x04\x0e\x03\x04\x01_\x02\x03\xcd\x04m\x04\x05\x01_\x04\x04\xcd\x04\a\x05\x06\x01E\t\x00f\b\x05%\x01\x0eGC (dedicated)%\x02\x0fGC (fractional)%\x03\tGC (idle)\x84\xa9\x01\f\a\xcd\x05\x9e\x12\x06\b\t%\x04\x05task0\xed\x06\xbc\x02\x01\x00\x04\n\xcd\x05\xf4\x02\a\v\fYP\rf\x8f\x02\a%\x05\x05span0\xef\x05>\x01\x00\x05\x0e%\x06\x05span1\xef\x06\xcc\x01\x01\x00\x06\x0f%\a\x04key0\xf0\x05\xb6\x01\x01\a\x10\x100123456789abcdef\xef\x05U\x01\x01\x06\x0f\xef\x05:\x01\x01\x05\x0e\xae.\x01\x11\xa7M\x05\x12\x0f\xdf\x01f\x19\x05%\b\x11pre-existing span\xef\x05\x1e\x00\x01\b\x13%\t\x12post-existing span\xef\x06\x89\x01\x00\x00\t\x14Q\xce\x10\x16A\x01\x97\x9b\xff\x99\xd5\xf9\x91\x01E\a\x03\x064E\xd5\v\x06\x8e\xe5\x18\x06\x01ao\x80\xc0&a\"\x80\x80'T\xa0\x01\x15\x06\xba\x01\x02\x83Ă\x1aA\x00\xd1\xe5\xff\x99\xd5\xf9\x91\x01%\n\x15runtime.startTheWorld%\v:/usr/local/google/home/hakim/golang/go/src/runtime/proc.go%\f\x12runtime.StartTrace%\r;/usr/local/google/home/hakim/golang/go/src/runtime/trace.go%\x0e\x13runtime/trace.Start%\x0fA/usr/local/google/home/hakim/golang/go/src/runtime/trace/trace.go%\x10#runtime/trace_test.TestUserTaskSpan%\x11K/usr/local/google/home/hakim/golang/go/src/runtime/trace/annotation_test.go%\x12\x0ftesting.tRunner%\x13=/usr/local/google/home/hakim/golang/go/src/testing/testing.go\xc3(\a\x05\x9c\xfc\x8b\x02\n\v\xcd\a\xa4֒\x02\f\r\x8b\x02\xc5گ\x02\x0e\x0f|\xe2\xe3\xd1\x02\x10\x11\x14\xbf\xa6\xb1\x02\x12\x13\x89\x06%\x14\x11runtime.ReadTrace%\x15\x19runtime/trace.Start.func1\xc3\x12\x15\x02\xf5\xe7\x92\x02\x14\r\x8b\x03\xd6ޯ\x02\x15\x0f\x81\x01%\x16\x16sync.(*WaitGroup).Wait%\x17/usr/local/google/home/hakim/golang/go/src/runtime/mgcsweep.go\xc3\t\x04\x01\xb0\xeb\x87\x02\x1b\x1c.\xc3\x11\f\x02\xb6\xe6\xd1\x02\x10\x11\x1c\xbf\xa6\xb1\x02\x12\x13\x89\x06%\x1d-runtime/trace_test.TestUserTaskSpan.func1.1.1%\x1e\x16runtime/trace.WithSpan%\x1fF/usr/local/google/home/hakim/golang/go/src/runtime/trace/annotation.go% +runtime/trace_test.TestUserTaskSpan.func1.1%!)runtime/trace_test.TestUserTaskSpan.func1\xc3'\x10\x05ޛ\xd3\x02\x1d\x11#\xcbׯ\x02\x1e\x1f\x8d\x01Ŝ\xd3\x02 \x11\"\xcbׯ\x02\x1e\x1f\x8d\x01\xf9\x9d\xd3\x02!\x11 %\"\fruntime.main\xc3\t\x02\x01\xb0\xa8\x8b\x02\"\vm\xc3\x11\x01\x02\xe2\xe3\xd1\x02\x10\x11\x14\xbf\xa6\xb1\x02\x12\x13\x89\x06%#\x14runtime.traceGoSched%$\x11runtime.StopTrace%%\x12runtime/trace.Stop\xc3)\x16\x05\xba\xb7\x93\x02#\r\xa4\b\x8dؒ\x02$\r\x9f\x02\xc4ܯ\x02%\x0f\x93\x01\x99\xe7\xd1\x02\x10\x11/\xbf\xa6\xb1\x02\x12\x13\x89\x06%&\x1eruntime/trace.NewContext.func1\xc3\x10\x11\x02\x99ݯ\x02&\x1f*\xfa\x9d\xd3\x02!\x11'\xc3\x18\x0f\x03Ŝ\xd3\x02 \x11\"\xcbׯ\x02\x1e\x1f\x8d\x01\xf9\x9d\xd3\x02!\x11 \xc3\t\v\x01\xe0\x9c\xd3\x02!\x11\x1c\xc3\x11\n\x02\xa9\xe5\xd1\x02\x10\x11\x1a\xbf\xa6\xb1\x02\x12\x13\x89\x06\xc3\t\x0e\x01\xf9\x9d\xd3\x02!\x11 %'\x15sync.(*WaitGroup).Add%(\x16sync.(*WaitGroup).Done\xc3\x17\x12\x03\xdf\xf7\x98\x02'\x17\\\xb3\xf9\x98\x02(\x17b\xfa\x9d\xd3\x02!\x11'\xc3\n\x06\x01\xf0\xa4\xb1\x02\x12\x13\xd1\x05") -------------------------------------------------------------------------------- /cmd/gotraceui/testdata/fuzz/FuzzLoadTrace/1be32de1435cb5898c1b913b1bc1b14a90a9a5475ed5ee12850cbf3274e7adfb: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("go 1.11 trace\x00\x00\x00A00̓\x83\x83\x83\xbd\x83\x83\x83\x831") 3 | -------------------------------------------------------------------------------- /cmd/gotraceui/testdata/fuzz/FuzzLoadTrace/331cc7da77f3f5aecb99448b1bf73d42eed38e307b313eef231e1fb6e255812f: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("go 1.11 trace\x00\x00\x00A00e0\xf2\xff\xff\xff\xff\xff\xff\xff\xff1\x020") 3 | -------------------------------------------------------------------------------- /cmd/gotraceui/testdata/fuzz/FuzzLoadTrace/61e0412ed82a1fedd57896cba5eec7ff5efc58917260e5b129f035c77535da71: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("go 1.11 trace\x00\x00\x00A0\x99\xd5\xf9\x910\x020") 3 | -------------------------------------------------------------------------------- /cmd/gotraceui/testdata/fuzz/FuzzLoadTrace/78cdf18e1b5aca794c4b57fda7acf1303f3ebfdfd6a1397f012af6b6e4847b27: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("go 1.11 trace\x00\x00\x00A\x01_\x02\x04\xcd\x04\a\x05\t\x00f\b\x05\xcd\x05\xf4\x02\a\v\fYn0\xef\x05>\x00\x00\x11'\xc3\n\x06\x01\xf0\xa4\xb1\x02\x12\x13\xd1\x00") 3 | -------------------------------------------------------------------------------- /cmd/gotraceui/testdata/fuzz/FuzzLoadTrace/962d74bd35140192c61ba2e9c66392c00d8d4e87a47f5224085f25511c9e1245: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("go 1.19 trace\x00\x00\x00A8C10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") 3 | -------------------------------------------------------------------------------- /cmd/gotraceui/testdata/fuzz/FuzzLoadTrace/9eed0f96b06667bd2b52f5795aaceccb5a29416e13ff603796727183a6d1c765: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("go 1.11 trace\x00\x00\x00A00\xcd\x040000\xcd\x040000X00X00X00X00X00X00A10\xcd\x040000") 3 | -------------------------------------------------------------------------------- /cmd/gotraceui/testdata/fuzz/FuzzLoadTrace/a2fa581a51bdd48d070564e9bddd969e212748249f480c87867b5ae2e46aff45: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("go 1.11 trace\x00\x00\x0000") 3 | -------------------------------------------------------------------------------- /cmd/gotraceui/testdata/fuzz/FuzzLoadTrace/ecad2f142ca420d4: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("go 1.11 trace\x00\x00\x00A00\xcd\x040\x0100f0\x01\xc5\x05\x97\x8e\xa000X00\x020") 3 | -------------------------------------------------------------------------------- /cmd/gotraceui/text.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | rtrace "runtime/trace" 6 | 7 | "honnef.co/go/gotraceui/clip" 8 | ourfont "honnef.co/go/gotraceui/font" 9 | "honnef.co/go/gotraceui/gesture" 10 | "honnef.co/go/gotraceui/layout" 11 | "honnef.co/go/gotraceui/theme" 12 | 13 | "gioui.org/font" 14 | "gioui.org/io/pointer" 15 | "gioui.org/text" 16 | "gioui.org/x/styledtext" 17 | ) 18 | 19 | type Text struct { 20 | // The theme must only be used for building the Text, with methods like Span. The Layout function has to use the 21 | // theme provided to it, to avoid race conditions when texts transition from widgets to independent windows. 22 | // 23 | // The theme must be reset in Reset. 24 | styles []styledtext.SpanStyle 25 | Alignment text.Alignment 26 | 27 | events []TextEvent 28 | hovered *TextSpan 29 | 30 | // Clickables we use for spans and reuse between frames. We allocate them one by one because it really doesn't 31 | // matter; we have hundreds of these at most. This won't make the GC sweat, and it avoids us having to do a bunch of 32 | // semi-manual memory management. 33 | clickables []*gesture.Click 34 | } 35 | 36 | type TextBuilder struct { 37 | Window *theme.Window 38 | Spans []TextSpan 39 | } 40 | 41 | type TextEvent struct { 42 | Span *TextSpan 43 | Event gesture.ClickEvent 44 | } 45 | 46 | type TextSpan struct { 47 | styledtext.SpanStyle 48 | ObjectLink ObjectLink 49 | 50 | Click *gesture.Click 51 | } 52 | 53 | func (txt *TextBuilder) Add(s TextSpan) { 54 | txt.Spans = append(txt.Spans, s) 55 | } 56 | 57 | func (txt *TextBuilder) Span(label string) *TextSpan { 58 | style := styledtext.SpanStyle{ 59 | Content: label, 60 | Size: txt.Window.Theme.TextSize, 61 | Color: txt.Window.ConvertColor(txt.Window.Theme.Palette.Foreground), 62 | Font: ourfont.Collection()[0].Font, 63 | } 64 | s := TextSpan{ 65 | SpanStyle: style, 66 | } 67 | txt.Spans = append(txt.Spans, s) 68 | return &txt.Spans[len(txt.Spans)-1] 69 | } 70 | 71 | func (txt *TextBuilder) SpanWith(label string, fn func(s *TextSpan)) *TextSpan { 72 | s := txt.Span(label) 73 | fn(s) 74 | return s 75 | } 76 | 77 | func (txt *TextBuilder) Bold(label string) *TextSpan { 78 | s := txt.Span(label) 79 | s.Font.Weight = font.Bold 80 | return s 81 | } 82 | 83 | func (txt *TextBuilder) DefaultLink(label, provenance string, obj any) *TextSpan { 84 | return txt.Link(label, defaultObjectLink(obj, provenance)) 85 | } 86 | 87 | func (txt *TextBuilder) Link(label string, link ObjectLink) *TextSpan { 88 | s := txt.Span(label) 89 | s.ObjectLink = link 90 | a := link.Action(0) 91 | switch a.(type) { 92 | case NavigationAction: 93 | s.Color = txt.Window.ConvertColor(txt.Window.Theme.Palette.NavigationLink) 94 | case OpenAction: 95 | s.Color = txt.Window.ConvertColor(txt.Window.Theme.Palette.OpenLink) 96 | default: 97 | s.Color = txt.Window.ConvertColor(txt.Window.Theme.Palette.Link) 98 | } 99 | return s 100 | } 101 | 102 | func (txt *Text) Reset(th *theme.Theme) { 103 | txt.events = txt.events[:0] 104 | txt.styles = txt.styles[:0] 105 | txt.Alignment = 0 106 | } 107 | 108 | // HoveredLink returns the link that was hovered in the last call to Layout. 109 | func (txt *Text) HoveredLink() ObjectLink { 110 | if txt.hovered == nil { 111 | return nil 112 | } else { 113 | return txt.hovered.ObjectLink 114 | } 115 | } 116 | 117 | // Update updates Text and returns the events that happened. The returned slice is only valid until the next call to 118 | // Update or Reset. 119 | func (txt *Text) Update(gtx layout.Context, spans []TextSpan) []TextEvent { 120 | out := txt.events[:0] 121 | 122 | txt.hovered = nil 123 | for i := range spans { 124 | s := &spans[i] 125 | if s.Click != nil { 126 | for _, ev := range s.Click.Update(gtx.Queue) { 127 | out = append(out, TextEvent{s, ev}) 128 | } 129 | if s.Click.Hovered() { 130 | txt.hovered = s 131 | } 132 | } 133 | } 134 | 135 | txt.events = out[:0] 136 | 137 | return out 138 | } 139 | 140 | func (txt *Text) Layout(win *theme.Window, gtx layout.Context, spans []TextSpan) layout.Dimensions { 141 | defer rtrace.StartRegion(context.Background(), "main.Text.Layout").End() 142 | 143 | var clickableIdx int 144 | for i := range spans { 145 | s := &spans[i] 146 | if s.ObjectLink != nil { 147 | var clk *gesture.Click 148 | if clickableIdx < len(txt.clickables) { 149 | clk = txt.clickables[clickableIdx] 150 | clickableIdx++ 151 | } else { 152 | clk = &gesture.Click{} 153 | txt.clickables = append(txt.clickables, clk) 154 | clickableIdx++ 155 | } 156 | s.Click = clk 157 | } 158 | } 159 | 160 | txt.styles = txt.styles[:0] 161 | for _, s := range spans { 162 | txt.styles = append(txt.styles, s.SpanStyle) 163 | } 164 | ptxt := styledtext.Text(win.Theme.Shaper, txt.styles...) 165 | ptxt.Alignment = txt.Alignment 166 | if txt.Alignment == text.Start { 167 | gtx.Constraints.Max.X = 1e6 168 | } 169 | return ptxt.Layout(gtx, func(gtx layout.Context, i int, dims layout.Dimensions) { 170 | defer clip.Rect{Max: dims.Size}.Push(gtx.Ops).Pop() 171 | s := &spans[i] 172 | if s.ObjectLink != nil { 173 | s.Click.Add(gtx.Ops) 174 | pointer.CursorPointer.Add(gtx.Ops) 175 | } 176 | }) 177 | } 178 | -------------------------------------------------------------------------------- /cmd/gotraceui/trace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "honnef.co/go/gotraceui/trace/ptrace" 5 | 6 | exptrace "golang.org/x/exp/trace" 7 | ) 8 | 9 | type Trace struct { 10 | *ptrace.Trace 11 | 12 | // The offset to apply to all timestamps from the trace. 13 | TimeOffset exptrace.Time 14 | 15 | GOROOT string 16 | GOPATH string 17 | 18 | allGoroutineSpanLabels [][]string 19 | allProcessorSpanLabels [][]string 20 | } 21 | 22 | // AdjustedTime represents a timestamp with the time offset already applied. 23 | type AdjustedTime exptrace.Time 24 | 25 | func (t *Trace) AdjustedTime(ts exptrace.Time) AdjustedTime { 26 | return AdjustedTime(ts + t.TimeOffset) 27 | } 28 | 29 | func (t *Trace) UnadjustedTime(ts AdjustedTime) exptrace.Time { 30 | return exptrace.Time(ts) - t.TimeOffset 31 | } 32 | 33 | func (t *Trace) goroutineSpanLabels(g *ptrace.Goroutine) []string { 34 | return t.allGoroutineSpanLabels[g.SeqID] 35 | } 36 | 37 | func (t *Trace) processorSpanLabels(p *ptrace.Processor) []string { 38 | return t.allProcessorSpanLabels[p.SeqID] 39 | } 40 | -------------------------------------------------------------------------------- /cmd/gotraceui/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "honnef.co/go/gotraceui/layout" 8 | "honnef.co/go/gotraceui/mem" 9 | "honnef.co/go/gotraceui/theme" 10 | "honnef.co/go/gotraceui/trace/ptrace" 11 | "honnef.co/go/gotraceui/widget" 12 | 13 | "gioui.org/font" 14 | "gioui.org/text" 15 | exptrace "golang.org/x/exp/trace" 16 | ) 17 | 18 | func scale[T float32 | float64](oldStart, oldEnd, newStart, newEnd, v T) T { 19 | slope := (newEnd - newStart) / (oldEnd - oldStart) 20 | output := newStart + slope*(v-oldStart) 21 | return output 22 | } 23 | 24 | func last[E any, S ~[]E](s S) E { 25 | return s[len(s)-1] 26 | } 27 | 28 | func lastPtr[E any, S ~[]E](s S) *E { 29 | return &s[len(s)-1] 30 | } 31 | 32 | type CellFormatter struct { 33 | Clicks mem.BucketSlice[Link] 34 | nfTs *NumberFormatter[AdjustedTime] 35 | nfUint64 *NumberFormatter[uint64] 36 | nfInt *NumberFormatter[int] 37 | } 38 | 39 | func (cf *CellFormatter) Reset() { 40 | cf.Clicks.Reset() 41 | if cf.nfTs == nil { 42 | cf.nfTs = NewNumberFormatter[AdjustedTime](local) 43 | cf.nfUint64 = NewNumberFormatter[uint64](local) 44 | cf.nfInt = NewNumberFormatter[int](local) 45 | } 46 | } 47 | 48 | func (cf *CellFormatter) Update(win *theme.Window, gtx layout.Context) { 49 | handleLinkClicks(win, gtx, &cf.Clicks) 50 | cf.Reset() 51 | } 52 | 53 | func (cl *CellFormatter) HoveredLink() ObjectLink { 54 | for i, n := 0, cl.Clicks.Len(); i < n; i++ { 55 | c := cl.Clicks.Ptr(i) 56 | if c.Click.Hovered() { 57 | return c.Link 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | func (cf *CellFormatter) Timestamp(win *theme.Window, gtx layout.Context, tr *Trace, ts exptrace.Time, label string) layout.Dimensions { 64 | return layout.RightAligned(gtx, func(gtx layout.Context) layout.Dimensions { 65 | link := cf.Clicks.Grow() 66 | link.Link = &TimestampObjectLink{Timestamp: ts} 67 | return link.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 68 | if label == "" { 69 | label = formatTimestamp(cf.nfTs, tr.AdjustedTime(ts)) 70 | } 71 | return widget.Label{ 72 | MaxLines: 1, 73 | Alignment: text.Start, 74 | }.Layout(gtx, win.Theme.Shaper, font.Font{}, 12, label, win.ColorMaterial(gtx, win.Theme.Palette.NavigationLink)) 75 | }) 76 | }) 77 | } 78 | 79 | func (cf *CellFormatter) Goroutine(win *theme.Window, gtx layout.Context, g *ptrace.Goroutine, label string) layout.Dimensions { 80 | return layout.RightAligned(gtx, func(gtx layout.Context) layout.Dimensions { 81 | link := cf.Clicks.Grow() 82 | link.Link = &GoroutineObjectLink{Goroutine: g} 83 | return link.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 84 | if label == "" { 85 | label = cf.nfUint64.Format("%d", uint64(g.ID)) 86 | } 87 | return widget.Label{ 88 | MaxLines: 1, 89 | Alignment: text.Start, 90 | }.Layout(gtx, win.Theme.Shaper, font.Font{}, 12, label, win.ColorMaterial(gtx, win.Theme.Palette.OpenLink)) 91 | }) 92 | }) 93 | } 94 | 95 | func (cf *CellFormatter) Task(win *theme.Window, gtx layout.Context, t *ptrace.Task, label string) layout.Dimensions { 96 | return layout.RightAligned(gtx, func(gtx layout.Context) layout.Dimensions { 97 | link := cf.Clicks.Grow() 98 | link.Link = &TaskObjectLink{Task: t} 99 | return link.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 100 | if label == "" { 101 | label = cf.nfUint64.Format("%d", uint64(t.ID)) 102 | } 103 | return widget.Label{ 104 | MaxLines: 1, 105 | Alignment: text.Start, 106 | }.Layout(gtx, win.Theme.Shaper, font.Font{}, 12, label, win.ColorMaterial(gtx, win.Theme.Palette.OpenLink)) 107 | }) 108 | }) 109 | } 110 | 111 | func (cf *CellFormatter) Duration(win *theme.Window, gtx layout.Context, d time.Duration, approx bool) layout.Dimensions { 112 | return layout.RightAligned(gtx, func(gtx layout.Context) layout.Dimensions { 113 | value, unit := durationNumberFormatSITable.format(d) 114 | // XXX the unit should be set in monospace 115 | if approx { 116 | value = "≥ " + value 117 | } 118 | return widget.Label{ 119 | MaxLines: 1, 120 | Alignment: text.Start, 121 | }.Layout(gtx, win.Theme.Shaper, font.Font{}, 12, fmt.Sprintf("%s %s", value, unit), win.ColorMaterial(gtx, win.Theme.Palette.Foreground)) 122 | }) 123 | } 124 | 125 | func (cf *CellFormatter) Function(win *theme.Window, gtx layout.Context, fn *ptrace.Function) layout.Dimensions { 126 | if fn == nil { 127 | return layout.Dimensions{ 128 | Size: gtx.Constraints.Min, 129 | } 130 | } 131 | 132 | link := cf.Clicks.Grow() 133 | link.Link = &FunctionObjectLink{Function: fn} 134 | return link.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 135 | label := fn.Func 136 | return widget.Label{ 137 | MaxLines: 1, 138 | Alignment: text.Start, 139 | }.Layout(gtx, win.Theme.Shaper, font.Font{}, 12, label, win.ColorMaterial(gtx, win.Theme.Palette.OpenLink)) 140 | }) 141 | } 142 | 143 | func (cf *CellFormatter) Number(win *theme.Window, gtx layout.Context, num int) layout.Dimensions { 144 | label := cf.nfInt.Format("%d", num) 145 | return widget.Label{ 146 | MaxLines: 1, 147 | Alignment: text.End, 148 | }.Layout(gtx, win.Theme.Shaper, font.Font{}, 12, label, win.ColorMaterial(gtx, win.Theme.Palette.Foreground)) 149 | } 150 | 151 | func (cf *CellFormatter) EventID(win *theme.Window, gtx layout.Context, num ptrace.EventID) layout.Dimensions { 152 | label := cf.nfInt.Format("%d", int(num)) 153 | return widget.Label{ 154 | MaxLines: 1, 155 | Alignment: text.End, 156 | }.Layout(gtx, win.Theme.Shaper, font.Font{}, 12, label, win.ColorMaterial(gtx, win.Theme.Palette.Foreground)) 157 | } 158 | 159 | func (cf *CellFormatter) Text(win *theme.Window, gtx layout.Context, l string) layout.Dimensions { 160 | return widget.Label{MaxLines: 1}.Layout(gtx, win.Theme.Shaper, font.Font{}, 12, l, win.ColorMaterial(gtx, win.Theme.Palette.Foreground)) 161 | } 162 | 163 | func (cf *CellFormatter) Spans(win *theme.Window, gtx layout.Context, spans Items[ptrace.Span]) layout.Dimensions { 164 | link := cf.Clicks.Grow() 165 | link.Link = &SpansObjectLink{Spans: spans} 166 | return link.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 167 | label := "" 168 | return widget.Label{ 169 | MaxLines: 1, 170 | Alignment: text.Start, 171 | }.Layout(gtx, win.Theme.Shaper, font.Font{}, 12, label, win.ColorMaterial(gtx, win.Theme.Palette.OpenLink)) 172 | }) 173 | } 174 | -------------------------------------------------------------------------------- /cmd/gotraceui/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | rdebug "runtime/debug" 9 | ) 10 | 11 | const Version = "devel" 12 | 13 | // version returns a version descriptor and reports whether the 14 | // version is a known release. 15 | func version(human string) (_ string, known bool) { 16 | if human != "devel" { 17 | return human, true 18 | } 19 | v, ok := buildInfoVersion() 20 | if ok { 21 | return v, false 22 | } 23 | return "devel", false 24 | } 25 | 26 | func PrintVersion(human string) { 27 | human, release := version(human) 28 | 29 | if release { 30 | fmt.Printf("%s %s\n", filepath.Base(os.Args[0]), human) 31 | } else if human == "devel" { 32 | fmt.Printf("%s (no version)\n", filepath.Base(os.Args[0])) 33 | } else { 34 | fmt.Printf("%s (devel, %s)\n", filepath.Base(os.Args[0]), human) 35 | } 36 | } 37 | 38 | func PrintVerboseVersion(human string) { 39 | PrintVersion(human) 40 | fmt.Println() 41 | fmt.Println("Compiled with Go version:", runtime.Version()) 42 | printBuildInfo() 43 | } 44 | 45 | func printBuildInfo() { 46 | if info, ok := rdebug.ReadBuildInfo(); ok { 47 | fmt.Println("Main module:") 48 | printModule(&info.Main) 49 | fmt.Println("Dependencies:") 50 | for _, dep := range info.Deps { 51 | printModule(dep) 52 | } 53 | } else { 54 | fmt.Println("Built without Go modules") 55 | } 56 | } 57 | 58 | func buildInfoVersion() (string, bool) { 59 | info, ok := rdebug.ReadBuildInfo() 60 | if !ok { 61 | return "", false 62 | } 63 | if info.Main.Version == "(devel)" { 64 | return "", false 65 | } 66 | return info.Main.Version, true 67 | } 68 | 69 | func printModule(m *rdebug.Module) { 70 | fmt.Printf("\t%s", m.Path) 71 | if m.Version != "(devel)" { 72 | fmt.Printf("@%s", m.Version) 73 | } 74 | if m.Sum != "" { 75 | fmt.Printf(" (sum: %s)", m.Sum) 76 | } 77 | if m.Replace != nil { 78 | fmt.Printf(" (replace: %s)", m.Replace.Path) 79 | } 80 | fmt.Println() 81 | } 82 | -------------------------------------------------------------------------------- /container/option.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | type Option[T any] struct { 4 | v T 5 | set bool 6 | } 7 | 8 | func None[T any]() Option[T] { 9 | return Option[T]{ 10 | set: false, 11 | } 12 | } 13 | 14 | func Some[T any](v T) Option[T] { 15 | return Option[T]{ 16 | v: v, 17 | set: true, 18 | } 19 | } 20 | 21 | func (m Option[T]) Get() (T, bool) { 22 | return m.v, m.set 23 | } 24 | 25 | func (m Option[T]) GetOr(alt T) T { 26 | if m.set { 27 | return m.v 28 | } else { 29 | return alt 30 | } 31 | } 32 | 33 | func (m Option[T]) Set() bool { 34 | return m.set 35 | } 36 | 37 | func (m Option[T]) MustGet() T { 38 | if !m.set { 39 | panic("called MustGet on unset Option") 40 | } 41 | return m.v 42 | } 43 | -------------------------------------------------------------------------------- /container/set.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | type Set[T comparable] map[T]struct{} 4 | 5 | func (set Set[T]) Add(v T) { 6 | set[v] = struct{}{} 7 | } 8 | 9 | func (set Set[T]) Delete(v T) { 10 | delete(set, v) 11 | } 12 | -------------------------------------------------------------------------------- /doc/manual/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/doc/manual/images/logo.png -------------------------------------------------------------------------------- /doc/manual/images/olive.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/doc/manual/images/olive.jpg -------------------------------------------------------------------------------- /doc/manual/images/screenshots/flame-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/doc/manual/images/screenshots/flame-graph.png -------------------------------------------------------------------------------- /doc/manual/images/screenshots/heatmap-linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/doc/manual/images/screenshots/heatmap-linear.png -------------------------------------------------------------------------------- /doc/manual/images/screenshots/heatmap-ranked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/doc/manual/images/screenshots/heatmap-ranked.png -------------------------------------------------------------------------------- /doc/manual/images/screenshots/merged-span.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/doc/manual/images/screenshots/merged-span.png -------------------------------------------------------------------------------- /doc/manual/images/screenshots/pollable-io-span.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/doc/manual/images/screenshots/pollable-io-span.png -------------------------------------------------------------------------------- /doc/manual/images/screenshots/sampling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/doc/manual/images/screenshots/sampling.png -------------------------------------------------------------------------------- /doc/manual/images/screenshots/track_whiskers_spans_end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/doc/manual/images/screenshots/track_whiskers_spans_end.png -------------------------------------------------------------------------------- /doc/manual/images/screenshots/user-regions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/doc/manual/images/screenshots/user-regions.png -------------------------------------------------------------------------------- /doc/manual/style.css: -------------------------------------------------------------------------------- 1 | pre.src-golang:before { content: 'Go'; } 2 | 3 | .state-active { background-color: #4A8E4A; } 4 | .state-blockedGC { background-color: #B55A79; } 5 | .state-blockedHappensBefore { background-color: #DB7A75; } 6 | .state-blockedNet { background-color: #C9716C; } 7 | .state-blockedSyscall { background-color: #B95B57; } 8 | .state-blocked { background-color: #A94C49; } 9 | .state-GC { background-color: #8A68B8; } 10 | .state-inactive { background-color: #7C7C7C; } 11 | .state-ready { background-color: #008F9F; } 12 | .state-stuck { background-color: #000000; } 13 | .state-userRegion { background-color: #F8ABEE; } 14 | .state-stack { background-color: #7DBC7B; } 15 | .state-sampled { background-color: #A1DB9F; } 16 | 17 | figure img { 18 | display: block; 19 | max-width: 100%; 20 | margin-left: auto; 21 | margin-right: auto; 22 | } 23 | 24 | li, dd, p { 25 | text-align: justify; 26 | text-justify: inter-word; 27 | } 28 | 29 | figure.side-by-side > figure { 30 | display: inline-block; 31 | width: 40%; 32 | } 33 | 34 | figure.side-by-side > figure img { 35 | max-width: 100%; 36 | } 37 | 38 | figure.side-by-side > figure > figcaption { 39 | text-align: center; 40 | } 41 | 42 | kbd:not(.sequence, .menu) { 43 | background-color: #eee; 44 | border-radius: 3px; 45 | border: 1px solid #b4b4b4; 46 | box-shadow: 47 | 0 1px 1px rgba(0, 0, 0, 0.2), 48 | 0 2px 0 0 rgba(255, 255, 255, 0.7) inset; 49 | display: inline-block; 50 | font-weight: 700; 51 | line-height: 1; 52 | padding: 2px 4px; 53 | white-space: nowrap; 54 | } 55 | 56 | thead { 57 | border-bottom: 1px solid black; 58 | } 59 | 60 | td { 61 | padding: 3px; 62 | } 63 | 64 | td + td { 65 | padding-left: 1em; 66 | } 67 | 68 | aside { 69 | margin-left: 2em; 70 | padding-left: 1em; 71 | border-left: 5px solid blue; 72 | } 73 | -------------------------------------------------------------------------------- /f32color/rgba.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package f32color 4 | 5 | import "honnef.co/go/gotraceui/color" 6 | 7 | // MulAlpha applies the alpha to the color. 8 | func MulAlpha(c color.Oklch, alpha float32) color.Oklch { 9 | c.A *= alpha 10 | return c 11 | } 12 | 13 | // Disabled desaturates the color and multiplies alpha. 14 | // Multiplying alpha blends the color together more with the background. 15 | func Disabled(c color.Oklch) (d color.Oklch) { 16 | const r = 80 // blend ratio 17 | d = mix(c, color.Oklch{A: c.A, L: c.L, C: 0, H: c.H}, r) 18 | d = MulAlpha(d, 128+32) 19 | return 20 | } 21 | 22 | // mix mixes c1 and c2 weighted by (1 - a) and a respectively. 23 | func mix(c1, c2 color.Oklch, a float32) color.Oklch { 24 | return color.Oklch{ 25 | L: c1.L*a + c2.L*(1-a), 26 | C: c1.C*a + c2.C*(1-a), 27 | H: c1.H*a + c2.H*(1-a), 28 | A: c1.A*a + c2.A*(1-a), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1735047879, 24 | "narHash": "sha256-L57IuP6DNhYgImz3hLFu8zVOlNzMhSNcWoquBSHMyTw=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "79a93b5d52be77c87d2d082eecd25c0c1500cd80", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "repo": "nixpkgs", 33 | "type": "github" 34 | } 35 | }, 36 | "root": { 37 | "inputs": { 38 | "flake-utils": "flake-utils", 39 | "nixpkgs": "nixpkgs" 40 | } 41 | }, 42 | "systems": { 43 | "locked": { 44 | "lastModified": 1681028828, 45 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 46 | "owner": "nix-systems", 47 | "repo": "default", 48 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "nix-systems", 53 | "repo": "default", 54 | "type": "github" 55 | } 56 | } 57 | }, 58 | "root": "root", 59 | "version": 7 60 | } 61 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | }; 6 | outputs = { self, nixpkgs, flake-utils }: 7 | flake-utils.lib.eachSystem ["x86_64-linux" "x86_64-darwin" "aarch64-darwin"] (system: 8 | let 9 | pkgs = nixpkgs.legacyPackages.${system}; 10 | in 11 | { 12 | packages.gotraceui = pkgs.buildGo123Module { 13 | name = "gotraceui"; 14 | src = self; 15 | vendorHash = "sha256-lszJObdEN6/Mo94btf5AD6W5dmTx7ciQgJWgQZ05UiU="; 16 | 17 | subPackages = ["cmd/gotraceui"]; 18 | 19 | nativeBuildInputs = [ pkgs.pkg-config ]; 20 | buildInputs = with pkgs; 21 | (if stdenv.isLinux then [ 22 | vulkan-headers 23 | libxkbcommon 24 | wayland 25 | xorg.libX11 26 | xorg.libXcursor 27 | xorg.libXfixes 28 | libGL 29 | ] else if stdenv.isDarwin then [ 30 | darwin.apple_sdk_11_0.frameworks.Foundation 31 | darwin.apple_sdk_11_0.frameworks.Metal 32 | darwin.apple_sdk_11_0.frameworks.QuartzCore 33 | darwin.apple_sdk_11_0.frameworks.AppKit 34 | darwin.apple_sdk_11_0.MacOSX-SDK 35 | ] else [ ]); 36 | 37 | ldflags = ["-X gioui.org/app.ID=co.honnef.Gotraceui"]; 38 | 39 | postInstall = '' 40 | cp -r share $out/ 41 | ''; 42 | 43 | meta = with nixpkgs.lib; { 44 | description = "An efficient frontend for Go execution traces"; 45 | homepage = "https://github.com/dominikh/gotraceui"; 46 | license = licenses.mit; 47 | platforms = platforms.all; 48 | }; 49 | }; 50 | 51 | packages.default = self.packages.${system}.gotraceui; 52 | 53 | apps.gotraceui = flake-utils.lib.mkApp { drv = self.packages.${system}.gotraceui; }; 54 | apps.default = self.apps.${system}.gotraceui; 55 | 56 | devShells.gotraceui = pkgs.mkShell { 57 | inputsFrom = builtins.attrValues self.packages.${system}; 58 | nativeBuildInputs = [ pkgs.python3Packages.fonttools ]; 59 | }; 60 | 61 | devShells.default = self.devShells.${system}.gotraceui; 62 | } 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /font/README: -------------------------------------------------------------------------------- 1 | Use 2 | 3 | pyftsubset DejaVuSerif.ttf --unicodes-file=runes.txt --no-ignore-missing-unicodes --output-file=fallback.ttf 4 | 5 | to generate the fallback font. 6 | -------------------------------------------------------------------------------- /font/fallback.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/font/fallback.ttf -------------------------------------------------------------------------------- /font/font.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "sync" 7 | 8 | "gioui.org/font" 9 | "gioui.org/font/gofont" 10 | "gioui.org/font/opentype" 11 | ) 12 | 13 | //go:embed fallback.ttf 14 | var fallback []byte 15 | 16 | var ( 17 | once sync.Once 18 | collection []font.FontFace 19 | ) 20 | 21 | func Collection() []font.FontFace { 22 | once.Do(func() { 23 | c := gofont.Collection() 24 | 25 | face, err := opentype.Parse(fallback) 26 | if err != nil { 27 | panic(fmt.Errorf("failed to parse fallback font: %s", err)) 28 | } 29 | 30 | fc := font.FontFace{ 31 | Font: font.Font{ 32 | Typeface: "Fallback", 33 | }, 34 | Face: face, 35 | } 36 | 37 | c = append(c, fc) 38 | n := len(c) 39 | collection = c[:n:n] 40 | }) 41 | return collection 42 | } 43 | -------------------------------------------------------------------------------- /font/runes.txt: -------------------------------------------------------------------------------- 1 | U+2B05 # leftwards black arrow 2 | U+27A1 # black rightwards arrow; we'd prefer to use U+B295, but DejaVu Serif doesn't have it 3 | U+2318 # place of interest sign 4 | -------------------------------------------------------------------------------- /generate_icons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # The logo was drawn in 32x32, scaling without interpolation to smaller sizes looks terrible. If the DM wants to scale 4 | # with interpolation, it can do so on its own. Thus we only generate sizes larger than 32x32. 5 | for size in 32x32 48x48 64x64 72x72 96x96 128x128 192x192 256x256 512x512 1024x1024; do 6 | mkdir -p share/icons/hicolor/$size/apps 7 | convert images/icon.png -sample $sizex$size share/icons/hicolor/$size/apps/gotraceui.png 8 | done 9 | -------------------------------------------------------------------------------- /gesture/alias.go: -------------------------------------------------------------------------------- 1 | package gesture 2 | 3 | import "gioui.org/gesture" 4 | 5 | type Drag = gesture.Drag 6 | 7 | type Axis = gesture.Axis 8 | 9 | const ( 10 | Horizontal Axis = gesture.Horizontal 11 | Vertical Axis = gesture.Vertical 12 | Both Axis = gesture.Both 13 | ) 14 | -------------------------------------------------------------------------------- /gesture/gesture.go: -------------------------------------------------------------------------------- 1 | package gesture 2 | 3 | import ( 4 | "image" 5 | "time" 6 | 7 | "gioui.org/f32" 8 | "gioui.org/io/event" 9 | "gioui.org/io/key" 10 | "gioui.org/io/pointer" 11 | "gioui.org/op" 12 | ) 13 | 14 | // The duration is somewhat arbitrary. 15 | const doubleClickDuration = 200 * time.Millisecond 16 | 17 | const ( 18 | // KindPress is reported for the first pointer 19 | // press. 20 | KindPress ClickKind = iota 21 | // KindClick is reported when a click action 22 | // is complete. 23 | KindClick 24 | // KindCancel is reported when the gesture is 25 | // cancelled. 26 | KindCancel 27 | ) 28 | 29 | type clickButton struct { 30 | // clickedAt is the timestamp at which 31 | // the last click occurred. 32 | clickedAt time.Duration 33 | // clicks is incremented if successive clicks 34 | // are performed within a fixed duration. 35 | clicks int 36 | // pressed tracks whether the pointer is pressed. 37 | pressed bool 38 | // pid is the pointer.ID. 39 | pid pointer.ID 40 | } 41 | 42 | // Click detects click gestures in the form 43 | // of ClickEvents. 44 | type Click struct { 45 | // hovered tracks whether the pointer is inside the gesture. 46 | hovered bool 47 | // entered tracks whether an Enter event has been received. 48 | entered bool 49 | buttons [3]clickButton 50 | // The e.Buttons of the previous pointer event. 51 | prevButtons pointer.Buttons 52 | } 53 | 54 | // ClickEvent represent a click action, either a 55 | // KindPress for the beginning of a click or a 56 | // KindClick for a completed click. 57 | type ClickEvent struct { 58 | Kind ClickKind 59 | Position image.Point 60 | Source pointer.Source 61 | Modifiers key.Modifiers 62 | Button pointer.Buttons 63 | // NumClicks records successive clicks occurring 64 | // within a short duration of each other. 65 | NumClicks int 66 | } 67 | 68 | type ClickKind uint8 69 | 70 | // Add the handler to the operation list to receive click events. 71 | func (c *Click) Add(ops *op.Ops) { 72 | pointer.InputOp{ 73 | Tag: c, 74 | Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave, 75 | }.Add(ops) 76 | } 77 | 78 | // Hovered returns whether a pointer is inside the area. 79 | func (c *Click) Hovered() bool { 80 | return c.hovered 81 | } 82 | 83 | // Pressed returns whether a pointer is pressing. 84 | func (c *Click) Pressed(btn pointer.Buttons) bool { 85 | for i := 0; i < 3; i++ { 86 | if btn&(1< github.com/dominikh/gio v0.0.0-20250304191902-d9db142d2565 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= 2 | eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= 3 | gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= 4 | gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= 5 | gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= 6 | gioui.org/x v0.4.0 h1:H6DofC86KoG51wgzeeA4ujZDDfcIa8vbL+jD9SpF/D8= 7 | gioui.org/x v0.4.0/go.mod h1:YAoFl2lbeARk4LopDXHK1N7fBQJupPYDSm9maf6tFlM= 8 | git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI= 9 | git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo= 10 | github.com/dominikh/gio v0.0.0-20250304191902-d9db142d2565 h1:tDRFFEHZTO6BVTLV0KyWm8pMpTwDMUyw+dqwWevmwz4= 11 | github.com/dominikh/gio v0.0.0-20250304191902-d9db142d2565/go.mod h1:pEZhd8LYJOp/PCTTguy9ulUL303yt5r9DC0Bf6E7EvE= 12 | github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= 13 | github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= 14 | github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= 15 | github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= 16 | github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= 17 | github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 18 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 19 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 20 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 21 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 22 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 24 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 25 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= 26 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= 27 | golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0= 28 | golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= 29 | golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= 30 | golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= 31 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 32 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 33 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 34 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 35 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 36 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 37 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 39 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 40 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 41 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 47 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 48 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 49 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 50 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 51 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 52 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 53 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 54 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 55 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 56 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 57 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 58 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 59 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 60 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 61 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 62 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 63 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 64 | honnef.co/go/curve v0.0.0-20250106034005-bfbc0c6fe0cc h1:obxBLi/4kFCM1oKlrEoYEEplJ2267W+lfKsYaGSQ1Zs= 65 | honnef.co/go/curve v0.0.0-20250106034005-bfbc0c6fe0cc/go.mod h1:qoI+1aKyxvS7Ni/ZX2NKQe2S6PfL08gR3hTHK3/E+io= 66 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/images/icon.png -------------------------------------------------------------------------------- /layout/alias.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | _ "unsafe" 5 | 6 | "gioui.org/layout" 7 | ) 8 | 9 | type Context = layout.Context 10 | type Dimensions = layout.Dimensions 11 | type Constraints = layout.Constraints 12 | type Flex = layout.Flex 13 | type Alignment = layout.Alignment 14 | type Axis = layout.Axis 15 | type Direction = layout.Direction 16 | type FlexChild = layout.FlexChild 17 | type Spacer = layout.Spacer 18 | type Stack = layout.Stack 19 | type Widget = layout.Widget 20 | type Inset = layout.Inset 21 | type ListElement = layout.ListElement 22 | type Spacing = layout.Spacing 23 | 24 | //go:linkname axisCrossConstraint gioui.org/layout.Axis.crossConstraint 25 | func axisCrossConstraint(Axis, Constraints) (int, int) 26 | 27 | //go:linkname axisMainConstraint gioui.org/layout.Axis.mainConstraint 28 | func axisMainConstraint(Axis, Constraints) (int, int) 29 | 30 | //go:linkname axisConstraints gioui.org/layout.Axis.constraints 31 | func axisConstraints(Axis, int, int, int, int) Constraints 32 | 33 | const ( 34 | SpaceEnd = layout.SpaceEnd 35 | SpaceStart = layout.SpaceStart 36 | SpaceSides = layout.SpaceSides 37 | SpaceAround = layout.SpaceAround 38 | SpaceBetween = layout.SpaceBetween 39 | SpaceEvenly = layout.SpaceEvenly 40 | ) 41 | 42 | var UniformInset = layout.UniformInset 43 | var Rigid = layout.Rigid 44 | var Flexed = layout.Flexed 45 | var Exact = layout.Exact 46 | var Expanded = layout.Expanded 47 | var Stacked = layout.Stacked 48 | var NewContext = layout.NewContext 49 | 50 | const ( 51 | Start = layout.Start 52 | End = layout.End 53 | Middle = layout.Middle 54 | Baseline = layout.Baseline 55 | ) 56 | 57 | const ( 58 | NW = layout.NW 59 | N = layout.N 60 | NE = layout.NE 61 | E = layout.E 62 | SE = layout.SE 63 | S = layout.S 64 | SW = layout.SW 65 | W = layout.W 66 | Center = layout.Center 67 | ) 68 | 69 | const ( 70 | Horizontal = layout.Horizontal 71 | Vertical = layout.Vertical 72 | ) 73 | -------------------------------------------------------------------------------- /layout/layout.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "context" 5 | "image" 6 | rtrace "runtime/trace" 7 | 8 | "gioui.org/io/pointer" 9 | "gioui.org/layout" 10 | "gioui.org/op" 11 | "gioui.org/op/clip" 12 | "gioui.org/x/outlay" 13 | ) 14 | 15 | type SmallGrid struct { 16 | Grid outlay.Grid 17 | RowPadding int 18 | ColumnPadding int 19 | } 20 | 21 | func (sg SmallGrid) Layout(gtx Context, rows, cols int, sizeEstimator outlay.Cell, cellFunc outlay.Cell) Dimensions { 22 | defer rtrace.StartRegion(context.Background(), "layout.SmallGrid.Layout").End() 23 | 24 | colWidths := make([]int, cols) 25 | // Storing dims isn't strictly necessarily, since we only need to know the row height (which Grid assumes is the 26 | // same for each row) and the column widths, as outlay.Grid passes an exact constraint to the cell function with 27 | // those dimensions. However, as written, the code depends less on implementation details. 28 | dims := make([]Dimensions, rows*cols) 29 | 30 | for row := 0; row < rows; row++ { 31 | for col := 0; col < cols; col++ { 32 | dim := sizeEstimator(gtx, row, col) 33 | dims[row*cols+col] = dim 34 | if dim.Size.X > colWidths[col] { 35 | colWidths[col] = dim.Size.X 36 | } 37 | } 38 | } 39 | 40 | dimmer := func(axis Axis, index, constraint int) int { 41 | switch axis { 42 | case Vertical: 43 | // outlay.Grid doesn't support different row heights, so we can return any of them 44 | return dims[0].Size.Y + sg.RowPadding 45 | case Horizontal: 46 | return colWidths[index] + sg.ColumnPadding 47 | default: 48 | panic("unreachable") 49 | } 50 | } 51 | 52 | // outlay.Grid fills the Max constraint 53 | height := rows*(dims[0].Size.Y+sg.RowPadding) - sg.RowPadding 54 | var width int 55 | for _, cw := range colWidths { 56 | width += cw + sg.ColumnPadding 57 | } 58 | gtx.Constraints.Max = gtx.Constraints.Constrain(image.Pt(width, height)) 59 | wrapper := func(gtx Context, row, col int) Dimensions { 60 | ogtx := gtx 61 | gtx.Constraints.Min.X -= sg.ColumnPadding 62 | gtx.Constraints.Max.X -= sg.ColumnPadding 63 | dims := cellFunc(gtx, row, col) 64 | dims.Size = ogtx.Constraints.Constrain(dims.Size) 65 | return dims 66 | } 67 | return sg.Grid.Layout(gtx, rows, cols, dimmer, wrapper) 68 | } 69 | 70 | // PixelInset is like Inset, but using pixel coordinates instead of dp. 71 | type PixelInset struct { 72 | Top, Bottom, Left, Right int 73 | } 74 | 75 | func (in PixelInset) Layout(gtx Context, w Widget) Dimensions { 76 | defer rtrace.StartRegion(context.Background(), "layout.PixelInset.Layout").End() 77 | 78 | top := in.Top 79 | right := in.Right 80 | bottom := in.Bottom 81 | left := in.Left 82 | mcs := gtx.Constraints 83 | mcs.Max.X -= left + right 84 | if mcs.Max.X < 0 { 85 | left = 0 86 | right = 0 87 | mcs.Max.X = 0 88 | } 89 | if mcs.Min.X > mcs.Max.X { 90 | mcs.Min.X = mcs.Max.X 91 | } 92 | mcs.Max.Y -= top + bottom 93 | if mcs.Max.Y < 0 { 94 | bottom = 0 95 | top = 0 96 | mcs.Max.Y = 0 97 | } 98 | if mcs.Min.Y > mcs.Max.Y { 99 | mcs.Min.Y = mcs.Max.Y 100 | } 101 | gtx.Constraints = mcs 102 | trans := op.Offset(image.Pt(left, top)).Push(gtx.Ops) 103 | dims := w(gtx) 104 | trans.Pop() 105 | return Dimensions{ 106 | Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}), 107 | Baseline: dims.Baseline + bottom, 108 | } 109 | } 110 | 111 | func Normalize(c Constraints) Constraints { 112 | if c.Min.X < 0 { 113 | c.Min.X = 0 114 | } 115 | if c.Min.Y < 0 { 116 | c.Min.Y = 0 117 | } 118 | if c.Max.X < 0 { 119 | c.Max.X = 0 120 | } 121 | if c.Max.Y < 0 { 122 | c.Max.Y = 0 123 | } 124 | 125 | if c.Min.X > c.Max.X { 126 | c.Min.X = c.Max.X 127 | } 128 | if c.Min.Y > c.Max.Y { 129 | c.Min.Y = c.Max.Y 130 | } 131 | 132 | return c 133 | } 134 | 135 | func Main(a Axis, pt *image.Point) *int { 136 | if a == Horizontal { 137 | return &pt.X 138 | } 139 | return &pt.Y 140 | } 141 | 142 | func Cross(a Axis, pt *image.Point) *int { 143 | if a == Horizontal { 144 | return &pt.Y 145 | } 146 | return &pt.X 147 | } 148 | 149 | func Rigids(gtx Context, axis layout.Axis, ws ...Widget) layout.Dimensions { 150 | defer rtrace.StartRegion(context.Background(), "layout.Rigids").End() 151 | 152 | cs := gtx.Constraints 153 | _, mainMax := axisMainConstraint(axis, cs) 154 | crossMin, crossMax := axisCrossConstraint(axis, cs) 155 | remaining := mainMax 156 | maxCross := crossMin 157 | var mainSize int 158 | for _, child := range ws { 159 | cgtx := gtx 160 | cgtx.Constraints = axisConstraints(axis, 0, remaining, crossMin, crossMax) 161 | 162 | pt := axis.Convert(image.Pt(mainSize, 0)) 163 | trans := op.Offset(pt).Push(gtx.Ops) 164 | dims := child(cgtx) 165 | trans.Pop() 166 | mainSize += axis.Convert(dims.Size).X 167 | 168 | sz := axis.Convert(dims.Size).X 169 | remaining -= sz 170 | if remaining < 0 { 171 | remaining = 0 172 | } 173 | 174 | if c := axis.Convert(dims.Size).Y; c > maxCross { 175 | maxCross = c 176 | } 177 | } 178 | sz := axis.Convert(image.Pt(mainSize, maxCross)) 179 | sz = cs.Constrain(sz) 180 | return Dimensions{Size: sz} 181 | } 182 | 183 | func WithCursor(gtx Context, p pointer.Cursor, w Widget) layout.Dimensions { 184 | defer rtrace.StartRegion(context.Background(), "layout.WithCursor").End() 185 | 186 | r := op.Record(gtx.Ops) 187 | dims := w(gtx) 188 | m := r.Stop() 189 | 190 | defer clip.Rect{Max: dims.Size}.Push(gtx.Ops).Pop() 191 | p.Add(gtx.Ops) 192 | m.Add(gtx.Ops) 193 | return dims 194 | } 195 | 196 | func RightAligned(gtx Context, w Widget) layout.Dimensions { 197 | defer rtrace.StartRegion(context.Background(), "layout.RightAligned").End() 198 | 199 | ngtx := gtx 200 | r := op.Record(gtx.Ops) 201 | ngtx.Constraints.Min.X = 0 202 | dims := w(ngtx) 203 | m := r.Stop() 204 | 205 | if dims.Size.X < gtx.Constraints.Min.X { 206 | defer op.Offset(image.Pt(gtx.Constraints.Min.X-dims.Size.X, 0)).Push(gtx.Ops).Pop() 207 | } 208 | m.Add(gtx.Ops) 209 | 210 | return layout.Dimensions{ 211 | Size: image.Pt(max(gtx.Constraints.Min.X, dims.Size.X), dims.Size.Y), 212 | } 213 | } 214 | 215 | func MiddleAligned(gtx Context, w Widget) layout.Dimensions { 216 | defer rtrace.StartRegion(context.Background(), "layout.MiddleAligned").End() 217 | 218 | ngtx := gtx 219 | r := op.Record(gtx.Ops) 220 | ngtx.Constraints.Min.Y = 0 221 | dims := w(ngtx) 222 | m := r.Stop() 223 | 224 | if dims.Size.Y < gtx.Constraints.Min.Y { 225 | defer op.Offset(image.Pt(0, (gtx.Constraints.Min.Y-dims.Size.Y)/2)).Push(gtx.Ops).Pop() 226 | } 227 | m.Add(gtx.Ops) 228 | 229 | return layout.Dimensions{ 230 | Size: image.Pt(dims.Size.X, max(gtx.Constraints.Min.Y, dims.Size.Y)), 231 | } 232 | } 233 | 234 | func Overlay(gtx Context, w1 Widget, w2 Widget) layout.Dimensions { 235 | defer rtrace.StartRegion(context.Background(), "layout.Overlay").End() 236 | 237 | dims := w1(gtx) 238 | 239 | gtx.Constraints.Min = dims.Size 240 | gtx.Constraints.Max = dims.Size 241 | w2(gtx) 242 | 243 | return dims 244 | } 245 | -------------------------------------------------------------------------------- /mem/mem.go: -------------------------------------------------------------------------------- 1 | package mem 2 | 3 | import ( 4 | "math" 5 | 6 | "gioui.org/op" 7 | "golang.org/x/exp/constraints" 8 | ) 9 | 10 | const allocatorBucketSize = 64 11 | 12 | // BucketSlice is like a slice, but grows one bucket at a time, instead of growing exponentially. This allows 13 | // for overall lower memory usage when the total capacity isn't known ahead of time, at the cost of more 14 | // overall allocations. 15 | type BucketSlice[T any] struct { 16 | n int 17 | buckets [][]T 18 | } 19 | 20 | // Grow grows the slice by one and returns a pointer to the new element, without overwriting it. 21 | func (l *BucketSlice[T]) Grow() *T { 22 | a, b := l.index(l.n) 23 | if a >= len(l.buckets) { 24 | l.buckets = append(l.buckets, make([]T, allocatorBucketSize)) 25 | } 26 | ptr := &l.buckets[a][b] 27 | l.n++ 28 | return ptr 29 | } 30 | 31 | // GrowN grows the slice by n elements. 32 | func (l *BucketSlice[T]) GrowN(n int) { 33 | for i := 0; i < n; i++ { 34 | l.Grow() 35 | } 36 | } 37 | 38 | // Append appends v to the slice and returns a pointer to the new element. 39 | func (l *BucketSlice[T]) Append(v T) *T { 40 | ptr := l.Grow() 41 | *ptr = v 42 | return ptr 43 | } 44 | 45 | func (l *BucketSlice[T]) index(i int) (int, int) { 46 | // Doing the division on uint instead of int compiles this function to a shift and an AND (for power of 2 47 | // bucket sizes), versus a whole bunch of instructions for int. 48 | return int(uint(i) / allocatorBucketSize), int(uint(i) % allocatorBucketSize) 49 | } 50 | 51 | func (l *BucketSlice[T]) Ptr(i int) *T { 52 | a, b := l.index(i) 53 | return &l.buckets[a][b] 54 | } 55 | 56 | func (l *BucketSlice[T]) Get(i int) T { 57 | a, b := l.index(i) 58 | return l.buckets[a][b] 59 | } 60 | 61 | func (l *BucketSlice[T]) Set(i int, v T) { 62 | a, b := l.index(i) 63 | l.buckets[a][b] = v 64 | } 65 | 66 | func (l *BucketSlice[T]) Len() int { 67 | return l.n 68 | } 69 | 70 | func (l *BucketSlice[T]) Reset() { 71 | l.n = 0 72 | } 73 | 74 | func (l *BucketSlice[T]) Truncate(n int) { 75 | if n >= l.n { 76 | return 77 | } 78 | a, b := l.index(n) 79 | clear(l.buckets[a][b:]) 80 | for i := a + 1; i < len(l.buckets); i++ { 81 | clear(l.buckets[i]) 82 | } 83 | l.n = n 84 | } 85 | 86 | const largeAllocatorBucketSize = 524288 87 | 88 | type LargeBucketSlice[T any] struct { 89 | n int 90 | buckets [][]T 91 | } 92 | 93 | func (l *LargeBucketSlice[T]) Grow() *T { 94 | a, b := l.index(l.n) 95 | if a >= len(l.buckets) { 96 | l.buckets = append(l.buckets, make([]T, allocatorBucketSize)) 97 | } 98 | ptr := &l.buckets[a][b] 99 | l.n++ 100 | return ptr 101 | } 102 | 103 | func (l *LargeBucketSlice[T]) GrowN(n int) { 104 | for i := 0; i < n; i++ { 105 | l.Grow() 106 | } 107 | } 108 | 109 | // Append appends v to the slice and returns a pointer to the new element. 110 | func (l *LargeBucketSlice[T]) Append(v T) *T { 111 | ptr := l.Grow() 112 | *ptr = v 113 | return ptr 114 | } 115 | 116 | func (l *LargeBucketSlice[T]) index(i int) (int, int) { 117 | // Doing the division on uint instead of int compiles this function to a shift and an AND (for power of 2 118 | // bucket sizes), versus a whole bunch of instructions for int. 119 | return int(uint(i) / allocatorBucketSize), int(uint(i) % allocatorBucketSize) 120 | } 121 | 122 | func (l *LargeBucketSlice[T]) Ptr(i int) *T { 123 | a, b := l.index(i) 124 | return &l.buckets[a][b] 125 | } 126 | 127 | func (l *LargeBucketSlice[T]) Get(i int) T { 128 | a, b := l.index(i) 129 | return l.buckets[a][b] 130 | } 131 | 132 | func (l *LargeBucketSlice[T]) Set(i int, v T) { 133 | a, b := l.index(i) 134 | l.buckets[a][b] = v 135 | } 136 | 137 | func (l *LargeBucketSlice[T]) Len() int { return l.n } 138 | func (l *LargeBucketSlice[T]) Reset() { l.n = 0 } 139 | 140 | func (l *LargeBucketSlice[T]) Truncate(n int) { 141 | if n >= l.n { 142 | return 143 | } 144 | a, b := l.index(n) 145 | clear(l.buckets[a][b:]) 146 | for i := a + 1; i < len(l.buckets); i++ { 147 | clear(l.buckets[i]) 148 | } 149 | l.n = n 150 | } 151 | 152 | type ReusableOps struct { 153 | ops op.Ops 154 | } 155 | 156 | // get resets and returns an op.Ops 157 | func (rops *ReusableOps) Get() *op.Ops { 158 | rops.ops.Reset() 159 | return &rops.ops 160 | } 161 | 162 | // AllocationCache is a trivial cache of allocations. Put appends a value to a slice and Get pops a value from the 163 | // slice, or allocates a new value. 164 | type AllocationCache[T any] struct { 165 | items []*T 166 | } 167 | 168 | func (c *AllocationCache[T]) Put(x *T) { 169 | c.items = append(c.items, x) 170 | } 171 | 172 | func (c *AllocationCache[T]) Get() *T { 173 | if len(c.items) == 0 { 174 | return new(T) 175 | } else { 176 | item := c.items[len(c.items)-1] 177 | c.items = c.items[:len(c.items)-1] 178 | return item 179 | } 180 | } 181 | 182 | // GrowLen increases the slice's length by n elements. 183 | func GrowLen[S ~[]E, E any](s S, n int) S { 184 | return append(s, make([]E, n)...) 185 | } 186 | 187 | func EnsureLen[S ~[]E, E any](s S, n int) S { 188 | if len(s) >= n { 189 | return s 190 | } 191 | return GrowLen(s, n-len(s)) 192 | } 193 | 194 | type DenseMap[K constraints.Integer, V any] struct { 195 | offset uint64 196 | values []V 197 | sparse map[K]V 198 | } 199 | 200 | func MakeDenseMap[K constraints.Integer, V any](m map[K]V) DenseMap[K, V] { 201 | if len(m) == 0 { 202 | return DenseMap[K, V]{sparse: m} 203 | } 204 | 205 | min := uint64(math.MaxUint64) 206 | max := uint64(0) 207 | 208 | for k := range m { 209 | if k < 0 { 210 | panic("negative keys are not permitted") 211 | } 212 | if uint64(k) < min { 213 | min = uint64(k) 214 | } 215 | if uint64(k) > max { 216 | max = uint64(k) 217 | } 218 | } 219 | 220 | if max-min == math.MaxUint64 || max-min+1 > math.MaxInt { 221 | return DenseMap[K, V]{sparse: m} 222 | } 223 | 224 | size := int(max - min + 1) 225 | limit := 4 * len(m) 226 | if limit/4 != len(m) || size > limit { 227 | return DenseMap[K, V]{sparse: m} 228 | } 229 | 230 | values := make([]V, size) 231 | for k, v := range m { 232 | values[uint64(k)-min] = v 233 | } 234 | 235 | return DenseMap[K, V]{ 236 | offset: uint64(min), 237 | values: values, 238 | } 239 | } 240 | 241 | func (dm *DenseMap[K, V]) At(idx K) V { 242 | if dm.sparse != nil { 243 | return dm.sparse[K(idx)] 244 | } 245 | return dm.values[uint64(idx)-dm.offset] 246 | } 247 | -------------------------------------------------------------------------------- /mysync/sync.go: -------------------------------------------------------------------------------- 1 | package mysync 2 | 3 | import ( 4 | "runtime" 5 | "slices" 6 | "sync" 7 | ) 8 | 9 | func Map[S ~[]E, E, R any](items S, limit int, out []R, fn func(subitems S) (R, error)) ([]R, error) { 10 | if len(items) == 0 { 11 | return nil, nil 12 | } 13 | 14 | if limit <= 0 { 15 | limit = runtime.GOMAXPROCS(0) 16 | } 17 | 18 | if limit > len(items) { 19 | limit = len(items) 20 | } 21 | 22 | out = slices.Grow(out, limit)[:len(out)+limit] 23 | err := Distribute(items, limit, func(group int, step int, subitems S) error { 24 | res, err := fn(subitems) 25 | out[group] = res 26 | return err 27 | }) 28 | 29 | return out, err 30 | } 31 | 32 | func Distribute[S ~[]E, E any](items S, limit int, fn func(group int, step int, subitems S) error) error { 33 | if len(items) == 0 { 34 | return nil 35 | } 36 | 37 | if limit <= 0 { 38 | limit = runtime.GOMAXPROCS(0) 39 | } 40 | 41 | if limit > len(items) { 42 | limit = len(items) 43 | } 44 | 45 | step := len(items) / limit 46 | var muGerr sync.Mutex 47 | var gerr error 48 | var wg sync.WaitGroup 49 | wg.Add(limit) 50 | for g := 0; g < limit; g++ { 51 | g := g 52 | go func() { 53 | defer wg.Done() 54 | var subset S 55 | if g < limit-1 { 56 | subset = items[g*step : (g+1)*step] 57 | } else { 58 | subset = items[g*step:] 59 | } 60 | if err := fn(g, step, subset); err != nil { 61 | muGerr.Lock() 62 | if gerr == nil { 63 | gerr = err 64 | } 65 | muGerr.Unlock() 66 | } 67 | }() 68 | } 69 | wg.Wait() 70 | return gerr 71 | } 72 | 73 | type Mutex[T any] struct { 74 | mu sync.RWMutex 75 | v T 76 | } 77 | 78 | type MutexUnlock struct { 79 | mu *sync.RWMutex 80 | } 81 | 82 | type MutexRUnlock struct { 83 | mu *sync.RWMutex 84 | } 85 | 86 | func NewMutex[T any](v T) *Mutex[T] { 87 | return &Mutex[T]{v: v} 88 | } 89 | 90 | func (mu *Mutex[T]) Lock() (T, MutexUnlock) { 91 | mu.mu.Lock() 92 | return mu.v, MutexUnlock{&mu.mu} 93 | } 94 | 95 | func (mu *Mutex[T]) RLock() (T, MutexRUnlock) { 96 | mu.mu.RLock() 97 | return mu.v, MutexRUnlock{&mu.mu} 98 | } 99 | 100 | func (u MutexUnlock) Unlock() { u.mu.Unlock() } 101 | func (u MutexRUnlock) RUnlock() { u.mu.RUnlock() } 102 | 103 | type Pool[T any] struct { 104 | pool sync.Pool 105 | } 106 | 107 | func NewPool[T any](fn func() T) *Pool[T] { 108 | return &Pool[T]{ 109 | pool: sync.Pool{ 110 | New: func() any { 111 | return fn() 112 | }, 113 | }, 114 | } 115 | } 116 | 117 | func (p *Pool[T]) Put(v T) { 118 | p.pool.Put(v) 119 | } 120 | 121 | func (p *Pool[T]) Get() T { 122 | return p.pool.Get().(T) 123 | } 124 | -------------------------------------------------------------------------------- /seq/seq.go: -------------------------------------------------------------------------------- 1 | package seq 2 | -------------------------------------------------------------------------------- /share/applications/co.honnef.Gotraceui.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Version=1.4 4 | Name=Gotraceui 5 | GenericName=Trace Viewer 6 | Comment=View Go execution traces 7 | Icon=gotraceui 8 | Exec=gotraceui %f 9 | Terminal=false 10 | MimeType=application/x-go-execution-trace 11 | -------------------------------------------------------------------------------- /share/icons/hicolor/1024x1024/apps/gotraceui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/share/icons/hicolor/1024x1024/apps/gotraceui.png -------------------------------------------------------------------------------- /share/icons/hicolor/128x128/apps/gotraceui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/share/icons/hicolor/128x128/apps/gotraceui.png -------------------------------------------------------------------------------- /share/icons/hicolor/192x192/apps/gotraceui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/share/icons/hicolor/192x192/apps/gotraceui.png -------------------------------------------------------------------------------- /share/icons/hicolor/256x256/apps/gotraceui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/share/icons/hicolor/256x256/apps/gotraceui.png -------------------------------------------------------------------------------- /share/icons/hicolor/32x32/apps/gotraceui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/share/icons/hicolor/32x32/apps/gotraceui.png -------------------------------------------------------------------------------- /share/icons/hicolor/48x48/apps/gotraceui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/share/icons/hicolor/48x48/apps/gotraceui.png -------------------------------------------------------------------------------- /share/icons/hicolor/512x512/apps/gotraceui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/share/icons/hicolor/512x512/apps/gotraceui.png -------------------------------------------------------------------------------- /share/icons/hicolor/64x64/apps/gotraceui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/share/icons/hicolor/64x64/apps/gotraceui.png -------------------------------------------------------------------------------- /share/icons/hicolor/72x72/apps/gotraceui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/share/icons/hicolor/72x72/apps/gotraceui.png -------------------------------------------------------------------------------- /share/icons/hicolor/96x96/apps/gotraceui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikh/gotraceui/00289f5f4c1da3e13babd2389e533b069cd18e3c/share/icons/hicolor/96x96/apps/gotraceui.png -------------------------------------------------------------------------------- /share/mime/packages/x-gotraceui.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Go execution trace 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /slices/slices.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | func Pop[E any, S ~[]E](s S) (E, S, bool) { 4 | if len(s) == 0 { 5 | return *new(E), s, false 6 | } 7 | e := s[len(s)-1] 8 | s = s[:len(s)-1] 9 | return e, s, true 10 | } 11 | -------------------------------------------------------------------------------- /theme/animation.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | 8 | "gioui.org/op" 9 | "golang.org/x/exp/constraints" 10 | "honnef.co/go/gotraceui/layout" 11 | ) 12 | 13 | type EasingFunction func(float64) float64 14 | type LerpFunction[T any] func(start, end T, r float64) T 15 | 16 | func Lerp[T constraints.Integer | constraints.Float](start, end T, r float64) T { 17 | return start + T(float64(end-start)*r) 18 | } 19 | 20 | type Lerper[T any] interface { 21 | Lerp(end T, ratio float64) T 22 | } 23 | 24 | type Animation[T any] struct { 25 | StartValue T 26 | EndValue T 27 | StartTime time.Time 28 | Duration time.Duration 29 | Ease EasingFunction 30 | Lerp LerpFunction[T] 31 | 32 | active bool 33 | } 34 | 35 | func (anim *Animation[T]) Start(gtx layout.Context, v1, v2 T, d time.Duration, ease EasingFunction) { 36 | anim.StartValue = v1 37 | anim.EndValue = v2 38 | anim.StartTime = gtx.Now 39 | anim.Duration = d 40 | anim.Ease = ease 41 | anim.active = true 42 | defer op.InvalidateOp{}.Add(gtx.Ops) 43 | } 44 | 45 | func StartSimpleAnimation[T constraints.Integer | constraints.Float](gtx layout.Context, anim *Animation[T], v1, v2 T, d time.Duration, ease EasingFunction) { 46 | anim.Start(gtx, v1, v2, d, ease) 47 | anim.Lerp = Lerp 48 | } 49 | 50 | func (anim *Animation[T]) Value(gtx layout.Context) T { 51 | if !anim.active { 52 | return anim.EndValue 53 | } 54 | 55 | d := gtx.Now.Sub(anim.StartTime) 56 | if d > anim.Duration { 57 | anim.active = false 58 | return anim.EndValue 59 | } 60 | 61 | ratio := anim.Ease(float64(d) / float64(anim.Duration)) 62 | op.InvalidateOp{}.Add(gtx.Ops) 63 | 64 | if anim.Lerp == nil { 65 | if lerper, ok := any(anim.StartValue).(Lerper[T]); ok { 66 | return lerper.Lerp(anim.EndValue, ratio) 67 | } else { 68 | panic(fmt.Sprintf("anim.Lerp is nil and %T doesn't implement Lerper", anim.StartValue)) 69 | } 70 | } 71 | 72 | return anim.Lerp(anim.StartValue, anim.EndValue, ratio) 73 | } 74 | 75 | func (anim *Animation[T]) Cancel() { 76 | anim.active = false 77 | } 78 | 79 | func (anim *Animation[T]) Done() bool { 80 | return !anim.active 81 | } 82 | 83 | func EaseIn(power int) EasingFunction { 84 | switch power { 85 | case 1: 86 | return func(r float64) float64 { return r } 87 | case 2: 88 | return func(r float64) float64 { return r * r } 89 | case 3: 90 | return func(r float64) float64 { return r * r * r } 91 | case 4: 92 | return func(r float64) float64 { return r * r * r * r } 93 | default: 94 | return func(r float64) float64 { return math.Pow(r, float64(power)) } 95 | } 96 | } 97 | 98 | func EaseOut(power int) EasingFunction { 99 | switch power { 100 | case 1: 101 | return func(r float64) float64 { return r } 102 | case 2: 103 | return func(r float64) float64 { r = 1 - r; return 1 - r*r } 104 | case 3: 105 | return func(r float64) float64 { r = 1 - r; return 1 - r*r*r } 106 | case 4: 107 | return func(r float64) float64 { r = 1 - r; return 1 - r*r*r*r } 108 | default: 109 | return func(r float64) float64 { return 1 - math.Pow(1-r, float64(power)) } 110 | } 111 | } 112 | 113 | func EaseBezier(t float64) float64 { 114 | return t * t * (3.0 - 2.0*t) 115 | } 116 | -------------------------------------------------------------------------------- /theme/component.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "strconv" 5 | 6 | "honnef.co/go/gotraceui/layout" 7 | ) 8 | 9 | // ComponentState describes a component's current state or desired state. 10 | type ComponentState int 11 | 12 | const ( 13 | ComponentStateNone ComponentState = iota 14 | ComponentStatePanel 15 | ComponentStateTab 16 | ComponentStateWindow 17 | ComponentStateClosed 18 | ) 19 | 20 | func (state ComponentState) String() string { 21 | switch state { 22 | case ComponentStateNone: 23 | return "none" 24 | case ComponentStatePanel: 25 | return "panel" 26 | case ComponentStateTab: 27 | return "tab" 28 | case ComponentStateClosed: 29 | return "closed" 30 | default: 31 | return strconv.Itoa(int(state)) 32 | } 33 | } 34 | 35 | // A Component is a widget that can be displayed as a panel, tab, or in its own window. 36 | type Component interface { 37 | Layout(win *Window, gtx layout.Context) layout.Dimensions 38 | // Title is the title to display as the window or tab title. 39 | Title() string 40 | // WantsTransition indicates that the component wants to transition to a different state. It is not 41 | // guaranteed that the request will be honored. Calling WantsTransition clears the desired state 42 | // transition. 43 | // 44 | // This method is not thread-safe. 45 | WantsTransition(gtx layout.Context) ComponentState 46 | // Transition notifies the component of its new state. This is called at least once before drawing the 47 | // component for the first time. There may or may not be a transition to the closed state. 48 | Transition(state ComponentState) 49 | } 50 | -------------------------------------------------------------------------------- /theme/dialog.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "context" 5 | "image" 6 | rtrace "runtime/trace" 7 | 8 | "honnef.co/go/gotraceui/color" 9 | "honnef.co/go/gotraceui/layout" 10 | "honnef.co/go/gotraceui/widget" 11 | 12 | "gioui.org/font" 13 | "gioui.org/op" 14 | "gioui.org/op/clip" 15 | "gioui.org/unit" 16 | ) 17 | 18 | type DialogStyle struct { 19 | BorderWidth unit.Dp 20 | BorderColor color.Oklch 21 | Title string 22 | TitleSize unit.Sp 23 | TitleColor color.Oklch 24 | TitleBackground color.Oklch 25 | TitlePadding unit.Dp 26 | Background color.Oklch 27 | Padding unit.Dp 28 | } 29 | 30 | func Dialog(th *Theme, title string) DialogStyle { 31 | return DialogStyle{ 32 | BorderWidth: th.WindowBorder, 33 | BorderColor: th.Palette.Border, 34 | Title: title, 35 | TitleSize: th.TextSize, 36 | TitleColor: th.Palette.Popup.TitleForeground, 37 | TitleBackground: th.Palette.Popup.TitleBackground, 38 | TitlePadding: th.WindowPadding, 39 | Background: th.Palette.Popup.Background, 40 | Padding: th.WindowPadding * 2, 41 | } 42 | } 43 | 44 | func (ds DialogStyle) Layout(win *Window, gtx layout.Context, w Widget) layout.Dimensions { 45 | defer rtrace.StartRegion(context.Background(), "theme.DialogStyle.Layout").End() 46 | 47 | titleGtx := gtx 48 | titleGtx.Constraints.Min.Y = 0 49 | titleGtx.Constraints.Max.X -= 2 * gtx.Dp(ds.BorderWidth+ds.TitlePadding) 50 | titleGtx.Constraints.Max.Y -= 2 * gtx.Dp(ds.BorderWidth+ds.TitlePadding) 51 | titleGtx.Constraints = layout.Normalize(titleGtx.Constraints) 52 | 53 | m := op.Record(titleGtx.Ops) 54 | labelDims := widget.Label{MaxLines: 1}.Layout(titleGtx, win.Theme.Shaper, font.Font{Weight: font.Bold}, ds.TitleSize, ds.Title, win.ColorMaterial(gtx, ds.TitleColor)) 55 | labelCall := m.Stop() 56 | 57 | wGtx := gtx 58 | wGtx.Constraints.Min.Y -= labelDims.Size.Y 59 | wGtx.Constraints.Max.X -= 2 * gtx.Dp(ds.BorderWidth+ds.Padding) 60 | wGtx.Constraints.Max.Y -= 2 * gtx.Dp(ds.BorderWidth+ds.Padding) 61 | wGtx.Constraints.Max.Y -= labelDims.Size.Y + 2*gtx.Dp(ds.TitlePadding+ds.BorderWidth) 62 | wGtx.Constraints = layout.Normalize(wGtx.Constraints) 63 | 64 | var wDims layout.Dimensions 65 | var wCall op.CallOp 66 | m = op.Record(wGtx.Ops) 67 | wDims = w(win, wGtx) 68 | wCall = m.Stop() 69 | 70 | if labelDims.Size.X > wDims.Size.X { 71 | wDims.Size.X = labelDims.Size.X 72 | } else if wDims.Size.X > labelDims.Size.X { 73 | labelDims.Size.X = wDims.Size.X 74 | } 75 | 76 | return Bordered{Width: ds.BorderWidth, Color: ds.BorderColor}.Layout(win, gtx, func(win *Window, gtx layout.Context) layout.Dimensions { 77 | return layout.Rigids(gtx, layout.Vertical, 78 | func(gtx layout.Context) layout.Dimensions { 79 | defer clip.Rect{Max: image.Pt(wDims.Size.X+gtx.Dp(ds.Padding)*2, labelDims.Size.Y+gtx.Dp(ds.TitlePadding)*2)}.Push(gtx.Ops).Pop() 80 | Fill(win, gtx.Ops, ds.TitleBackground) 81 | return layout.UniformInset(ds.TitlePadding).Layout(gtx, func(gtx layout.Context) layout.Dimensions { 82 | labelCall.Add(gtx.Ops) 83 | return labelDims 84 | }) 85 | }, 86 | 87 | func(gtx layout.Context) layout.Dimensions { 88 | defer clip.Rect{Max: image.Pt(wDims.Size.X+gtx.Dp(ds.Padding)*2, wDims.Size.Y+gtx.Dp(ds.Padding)*2)}.Push(gtx.Ops).Pop() 89 | Fill(win, gtx.Ops, ds.Background) 90 | return layout.UniformInset(ds.Padding).Layout(gtx, func(gtx layout.Context) layout.Dimensions { 91 | wCall.Add(gtx.Ops) 92 | return wDims 93 | }) 94 | }, 95 | ) 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /theme/editor.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | // This is copied almost verbatim from material, but uses our theme. 4 | 5 | import ( 6 | "context" 7 | rtrace "runtime/trace" 8 | 9 | "honnef.co/go/gotraceui/color" 10 | "honnef.co/go/gotraceui/f32color" 11 | "honnef.co/go/gotraceui/layout" 12 | "honnef.co/go/gotraceui/widget" 13 | 14 | "gioui.org/font" 15 | "gioui.org/op" 16 | "gioui.org/op/paint" 17 | "gioui.org/text" 18 | "gioui.org/unit" 19 | ) 20 | 21 | type EditorStyle struct { 22 | Font font.Font 23 | TextSize unit.Sp 24 | // Color is the text color. 25 | Color color.Oklch 26 | // Hint contains the text displayed when the editor is empty. 27 | Hint string 28 | // HintColor is the color of hint text. 29 | HintColor color.Oklch 30 | // SelectionColor is the color of the background for selected text. 31 | SelectionColor color.Oklch 32 | Editor *widget.Editor 33 | 34 | shaper *text.Shaper 35 | } 36 | 37 | func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle { 38 | return EditorStyle{ 39 | Editor: editor, 40 | TextSize: th.TextSize, 41 | Color: th.Palette.Foreground, 42 | shaper: th.Shaper, 43 | Hint: hint, 44 | HintColor: f32color.MulAlpha(th.Palette.Foreground, 0.73), 45 | SelectionColor: th.Palette.PrimarySelection, 46 | } 47 | } 48 | 49 | func (e EditorStyle) Layout(win *Window, gtx layout.Context) layout.Dimensions { 50 | defer rtrace.StartRegion(context.Background(), "theme.EditorStyle.Layout").End() 51 | 52 | // Choose colors. 53 | textColorMacro := op.Record(gtx.Ops) 54 | paint.ColorOp{Color: win.ConvertColor(e.Color)}.Add(gtx.Ops) 55 | textColor := textColorMacro.Stop() 56 | hintColorMacro := op.Record(gtx.Ops) 57 | paint.ColorOp{Color: win.ConvertColor(e.HintColor)}.Add(gtx.Ops) 58 | hintColor := hintColorMacro.Stop() 59 | selectionColorMacro := op.Record(gtx.Ops) 60 | paint.ColorOp{Color: win.ConvertColor(blendDisabledColor(gtx.Queue == nil, e.SelectionColor))}.Add(gtx.Ops) 61 | selectionColor := selectionColorMacro.Stop() 62 | 63 | var maxlines int 64 | if e.Editor.SingleLine { 65 | maxlines = 1 66 | } 67 | 68 | macro := op.Record(gtx.Ops) 69 | tl := widget.Label{Alignment: e.Editor.Alignment, MaxLines: maxlines} 70 | dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint, hintColor) 71 | call := macro.Stop() 72 | 73 | if w := dims.Size.X; gtx.Constraints.Min.X < w { 74 | gtx.Constraints.Min.X = w 75 | } 76 | if h := dims.Size.Y; gtx.Constraints.Min.Y < h { 77 | gtx.Constraints.Min.Y = h 78 | } 79 | dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize, textColor, selectionColor) 80 | if e.Editor.Len() == 0 { 81 | call.Add(gtx.Ops) 82 | } 83 | return dims 84 | } 85 | 86 | func blendDisabledColor(disabled bool, c color.Oklch) color.Oklch { 87 | if disabled { 88 | return f32color.Disabled(c) 89 | } 90 | return c 91 | } 92 | 93 | type TextBoxStyle struct { 94 | EditorStyle 95 | Validate func(s string) bool 96 | } 97 | 98 | func TextBox(th *Theme, editor *widget.Editor, hint string) TextBoxStyle { 99 | return TextBoxStyle{EditorStyle: Editor(th, editor, hint)} 100 | } 101 | 102 | func (tb TextBoxStyle) Layout(win *Window, gtx layout.Context) layout.Dimensions { 103 | defer rtrace.StartRegion(context.Background(), "theme.TextBoxStyle.Layout").End() 104 | 105 | if tb.Validate != nil && !tb.Validate(tb.Editor.Text()) { 106 | tb.Color = oklch(62.8, 0.258, 29.234) 107 | } 108 | return Background{Color: oklch(100, 0, 0)}.Layout(win, gtx, func(win *Window, gtx layout.Context) layout.Dimensions { 109 | return Bordered{Color: oklch(0, 0, 0), Width: 1}.Layout(win, gtx, func(win *Window, gtx layout.Context) layout.Dimensions { 110 | return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions { 111 | return tb.EditorStyle.Layout(win, gtx) 112 | }) 113 | }) 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /theme/future.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "time" 5 | 6 | "gioui.org/app" 7 | ) 8 | 9 | type Future[T any] struct { 10 | // Concurrently accessed by computation function, controller, and user code 11 | result chan T 12 | 13 | // Accessed in the window goroutine 14 | // 15 | // The channel itself is accessed concurrently, but the field isn't 16 | cancelled chan struct{} 17 | done chan struct{} 18 | res T 19 | resSet bool 20 | read bool 21 | fn func(cancelled chan struct{}) 22 | ff *Futures 23 | } 24 | 25 | type future interface { 26 | wasRead() bool 27 | isDone() bool 28 | cancel() 29 | } 30 | 31 | func Immediate[T any](value T) *Future[T] { 32 | return &Future[T]{ 33 | res: value, 34 | resSet: true, 35 | } 36 | } 37 | 38 | func NewFuture[T any](win *Window, fn func(cancelled <-chan struct{}) T) *Future[T] { 39 | ft := &Future[T]{ 40 | cancelled: make(chan struct{}), 41 | result: make(chan T, 1), 42 | ff: win.Futures, 43 | // Don't immediately cancel future if it was created but not read from in this frame 44 | read: true, 45 | done: make(chan struct{}), 46 | } 47 | ft.fn = func(cancelled chan struct{}) { 48 | res := fn(cancelled) 49 | select { 50 | case <-cancelled: 51 | // We got cancelled, the return value is meaningless 52 | default: 53 | select { 54 | case ft.result <- res: 55 | // We've got the result of the computation. Invalidate the window so a new frame gets drawn with the 56 | // result. 57 | close(ft.done) 58 | win.AppWindow.Invalidate() 59 | default: 60 | // We've already gotten a valid result before and this goroutine raced with it. Discard the new result. 61 | // 62 | // Invalidate the frame, anyway, in case canceling raced with reading the value earlier. 63 | win.AppWindow.Invalidate() 64 | } 65 | } 66 | } 67 | 68 | ft.ff.add(ft) 69 | go ft.fn(ft.cancelled) 70 | 71 | return ft 72 | } 73 | 74 | func (ft *Future[T]) wasRead() bool { 75 | b := ft.read 76 | ft.read = false 77 | return b 78 | } 79 | 80 | func (ft *Future[T]) cancel() { 81 | close(ft.cancelled) 82 | } 83 | 84 | func (ft *Future[T]) MustResult() T { 85 | v, ok := ft.Result() 86 | if !ok { 87 | panic("Future wasn't ready") 88 | } 89 | return v 90 | } 91 | 92 | func (ft *Future[T]) Wait() T { 93 | if ft.resSet { 94 | return ft.res 95 | } 96 | <-ft.done 97 | return ft.MustResult() 98 | } 99 | 100 | func (ft *Future[T]) ResultNoWait() (T, bool) { 101 | if ft.resSet { 102 | // We already have the value 103 | return ft.res, true 104 | } 105 | 106 | // Prevent sweep from cancelling this future 107 | ft.read = true 108 | 109 | select { 110 | case res := <-ft.result: 111 | // First time reading the computed value 112 | ft.res = res 113 | ft.resSet = true 114 | return res, true 115 | case <-ft.cancelled: 116 | // Don't discard result if cancellation raced with the computation finishing. 117 | select { 118 | case res := <-ft.result: 119 | ft.res = res 120 | ft.resSet = true 121 | return res, true 122 | default: 123 | } 124 | 125 | // Future got cancelled and reused later, restart the computation 126 | ft.cancelled = make(chan struct{}) 127 | // Re-add the future to the controller so it gets swept again 128 | ft.ff.add(ft) 129 | go ft.fn(ft.cancelled) 130 | return *new(T), false 131 | default: 132 | // Not ready yet, we'll try again next frame 133 | return *new(T), false 134 | } 135 | } 136 | 137 | func (ft *Future[T]) Result() (T, bool) { 138 | if ft.resSet { 139 | // We already have the value 140 | return ft.res, true 141 | } 142 | 143 | // Prevent sweep from cancelling this future 144 | ft.read = true 145 | 146 | t := time.NewTimer(500 * time.Microsecond) 147 | defer t.Stop() 148 | select { 149 | case res := <-ft.result: 150 | // First time reading the computed value 151 | ft.res = res 152 | ft.resSet = true 153 | return res, true 154 | case <-ft.cancelled: 155 | // Don't discard result if cancellation raced with the computation finishing. 156 | select { 157 | case res := <-ft.result: 158 | ft.res = res 159 | ft.resSet = true 160 | return res, true 161 | default: 162 | } 163 | 164 | // Future got cancelled and reused later, restart the computation 165 | ft.cancelled = make(chan struct{}) 166 | // Re-add the future to the controller so it gets swept again 167 | ft.ff.add(ft) 168 | go ft.fn(ft.cancelled) 169 | return *new(T), false 170 | case <-t.C: 171 | // Not ready yet, we'll try again next frame 172 | return *new(T), false 173 | } 174 | } 175 | 176 | func (ft *Future[T]) isDone() bool { 177 | return ft.resSet 178 | } 179 | 180 | type Futures struct { 181 | win *app.Window 182 | futures []future 183 | } 184 | 185 | func (ff *Futures) Sweep() { 186 | n := ff.futures[:0] 187 | for _, ft := range ff.futures { 188 | if ft.isDone() { 189 | continue 190 | } 191 | 192 | if !ft.wasRead() { 193 | // The future wasn't read this frame. Cancel the work it is doing, under the assumption that it won't be 194 | // needed anymore. If it ends up being needed later (e.g. because the user went back to an old panel), the 195 | // future will restart itself. 196 | ft.cancel() 197 | continue 198 | } 199 | 200 | n = append(n, ft) 201 | } 202 | ff.futures = n 203 | } 204 | 205 | func (ff *Futures) add(ft future) { 206 | ff.futures = append(ff.futures, ft) 207 | } 208 | -------------------------------------------------------------------------------- /theme/label.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | // A copy of (our) widget.Label, with all actual rendering removed, used when we are only interested in computing the 4 | // dimensions. 5 | 6 | import ( 7 | "image" 8 | 9 | "gioui.org/font" 10 | "gioui.org/layout" 11 | "gioui.org/text" 12 | "gioui.org/unit" 13 | "honnef.co/go/gotraceui/widget" 14 | 15 | "golang.org/x/image/math/fixed" 16 | ) 17 | 18 | func labelDimensions(gtx layout.Context, l widget.Label, lt *text.Shaper, font font.Font, size unit.Sp, txt string) layout.Dimensions { 19 | cs := gtx.Constraints 20 | textSize := fixed.I(gtx.Sp(size)) 21 | lt.LayoutString(text.Parameters{ 22 | Font: font, 23 | PxPerEm: textSize, 24 | MaxLines: l.MaxLines, 25 | Truncator: l.Truncator, 26 | Alignment: l.Alignment, 27 | MaxWidth: cs.Max.X, 28 | MinWidth: cs.Min.X, 29 | Locale: gtx.Locale, 30 | }, txt) 31 | viewport := image.Rectangle{Max: cs.Max} 32 | it := textIterator{ 33 | viewport: viewport, 34 | maxLines: l.MaxLines, 35 | } 36 | for g, ok := lt.NextGlyph(); ok; g, ok = lt.NextGlyph() { 37 | if _, ok := it.processGlyph(g, true); !ok { 38 | break 39 | } 40 | } 41 | viewport.Min = viewport.Min.Add(it.padding.Min) 42 | viewport.Max = viewport.Max.Add(it.padding.Max) 43 | dims := layout.Dimensions{Size: it.bounds.Size()} 44 | dims.Size = cs.Constrain(dims.Size) 45 | dims.Baseline = dims.Size.Y - it.baseline 46 | return dims 47 | } 48 | 49 | // textIterator computes the bounding box of and paints text. 50 | type textIterator struct { 51 | // viewport is the rectangle of document coordinates that the iterator is 52 | // trying to fill with text. 53 | viewport image.Rectangle 54 | // maxLines is the maximum number of text lines that should be displayed. 55 | maxLines int 56 | 57 | // linesSeen tracks the quantity of line endings this iterator has seen. 58 | linesSeen int 59 | // padding is the space needed outside of the bounds of the text to ensure no 60 | // part of a glyph is clipped. 61 | padding image.Rectangle 62 | // bounds is the logical bounding box of the text. 63 | bounds image.Rectangle 64 | // visible tracks whether the most recently iterated glyph is visible within 65 | // the viewport. 66 | visible bool 67 | // first tracks whether the iterator has processed a glyph yet. 68 | first bool 69 | // baseline tracks the location of the first line of text's baseline. 70 | baseline int 71 | } 72 | 73 | // processGlyph checks whether the glyph is visible within the iterator's configured 74 | // viewport and (if so) updates the iterator's text dimensions to include the glyph. 75 | func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visibleOrBefore bool) { 76 | if !it.first && g.Flags&text.FlagTruncator != 0 { 77 | return g, false 78 | } 79 | 80 | if it.maxLines > 0 { 81 | if g.Flags&text.FlagLineBreak != 0 { 82 | it.linesSeen++ 83 | } 84 | if it.linesSeen == it.maxLines && g.Flags&text.FlagParagraphBreak != 0 { 85 | return g, false 86 | } 87 | } 88 | // Compute the maximum extent to which glyphs overhang on the horizontal 89 | // axis. 90 | if d := g.Bounds.Min.X.Floor(); d < it.padding.Min.X { 91 | it.padding.Min.X = d 92 | } 93 | if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.padding.Max.X { 94 | it.padding.Max.X = d 95 | } 96 | logicalBounds := image.Rectangle{ 97 | Min: image.Pt(g.X.Floor(), int(g.Y)-g.Ascent.Ceil()), 98 | Max: image.Pt((g.X + g.Advance).Ceil(), int(g.Y)+g.Descent.Ceil()), 99 | } 100 | if !it.first { 101 | it.first = true 102 | it.baseline = int(g.Y) 103 | it.bounds = logicalBounds 104 | } 105 | 106 | above := logicalBounds.Max.Y < it.viewport.Min.Y 107 | below := logicalBounds.Min.Y > it.viewport.Max.Y 108 | left := logicalBounds.Max.X < it.viewport.Min.X 109 | right := logicalBounds.Min.X > it.viewport.Max.X 110 | it.visible = !above && !below && !left && !right 111 | if it.visible { 112 | it.bounds.Min.X = min(it.bounds.Min.X, logicalBounds.Min.X) 113 | it.bounds.Min.Y = min(it.bounds.Min.Y, logicalBounds.Min.Y) 114 | it.bounds.Max.X = max(it.bounds.Max.X, logicalBounds.Max.X) 115 | it.bounds.Max.Y = max(it.bounds.Max.Y, logicalBounds.Max.Y) 116 | } 117 | return g, ok && !below 118 | } 119 | -------------------------------------------------------------------------------- /theme/list_window.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "context" 5 | rtrace "runtime/trace" 6 | 7 | "honnef.co/go/gotraceui/color" 8 | "honnef.co/go/gotraceui/layout" 9 | "honnef.co/go/gotraceui/widget" 10 | 11 | "gioui.org/font" 12 | "gioui.org/io/key" 13 | "gioui.org/op/clip" 14 | "gioui.org/x/eventx" 15 | ) 16 | 17 | type ListWindowItem struct { 18 | Item any 19 | Label string 20 | FilterLabels []string 21 | index int 22 | click widget.PrimaryClickable 23 | } 24 | 25 | type Filter interface { 26 | Filter(item ListWindowItem) bool 27 | } 28 | 29 | type ListWindow struct { 30 | BuildFilter func(string) Filter 31 | 32 | items []ListWindowItem 33 | 34 | filtered []int 35 | // index of the selected item in the filtered list 36 | index int 37 | done bool 38 | cancelled bool 39 | 40 | theme *Theme 41 | input widget.Editor 42 | list widget.List 43 | } 44 | 45 | func NewListWindow(th *Theme) *ListWindow { 46 | return &ListWindow{ 47 | theme: th, 48 | input: widget.Editor{ 49 | SingleLine: true, 50 | Submit: true, 51 | }, 52 | list: widget.List{ 53 | List: layout.List{ 54 | Axis: layout.Vertical, 55 | }, 56 | }, 57 | } 58 | } 59 | 60 | func (w *ListWindow) SetItems(items []ListWindowItem) { 61 | w.items = items 62 | w.filtered = make([]int, len(items)) 63 | for i := range w.items { 64 | w.items[i].index = i 65 | w.filtered[i] = i 66 | } 67 | } 68 | 69 | func (w *ListWindow) Cancelled() bool { return w.cancelled } 70 | func (w *ListWindow) Confirmed() (any, bool) { 71 | if !w.done { 72 | return nil, false 73 | } 74 | w.done = false 75 | return w.items[w.filtered[w.index]].Item, true 76 | } 77 | 78 | // Don't bubble up normal key presses. This is a workaround for Gio's input handling. The editor widget 79 | // primarily uses edit events to handle key presses, but edit events don't prevent key presses from bubbling up 80 | // the input tree. That means that typing in an editor can trigger our single-key shortcuts. 81 | var editorKeyset = key.Set("↓|↑|⎋|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z") 82 | 83 | func (w *ListWindow) Layout(win *Window, gtx layout.Context) layout.Dimensions { 84 | defer rtrace.StartRegion(context.Background(), "theme.ListWindow.Layout").End() 85 | defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() 86 | 87 | key.InputOp{Tag: w, Keys: editorKeyset}.Add(gtx.Ops) 88 | 89 | var spy *eventx.Spy 90 | 91 | dims := func() layout.Dimensions { 92 | defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() 93 | spy, gtx = eventx.Enspy(gtx) 94 | gtx.Constraints.Min.X = gtx.Constraints.Max.X 95 | 96 | fn2 := func(gtx layout.Context) layout.Dimensions { 97 | return List(w.theme, &w.list).Layout(win, gtx, len(w.filtered), func(gtx layout.Context, index int) layout.Dimensions { 98 | // XXX use constants for colors 99 | item := &w.items[w.filtered[index]] 100 | return item.click.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 101 | var c color.Oklch 102 | if index == w.index { 103 | c = oklch(62.8, 0.258, 29.234) 104 | } else if item.click.Hovered() { 105 | c = oklch(70.71, 0.322, 328.36) 106 | } else { 107 | c = oklch(0, 0, 0) 108 | } 109 | return widget.Label{MaxLines: 1}.Layout(gtx, w.theme.Shaper, font.Font{}, w.theme.TextSize, item.Label, win.ColorMaterial(gtx, c)) 110 | }) 111 | }) 112 | } 113 | 114 | flex := layout.Flex{ 115 | Axis: layout.Vertical, 116 | } 117 | editor := Editor(w.theme, &w.input, "") 118 | editor.Editor.Focus() 119 | return flex.Layout(gtx, layout.Rigid(Dumb(win, editor.Layout)), layout.Flexed(1, fn2)) 120 | }() 121 | 122 | // The editor widget selectively handles the up and down arrow keys, depending on the contents of the text field and 123 | // the position of the cursor. This means that our own InputOp won't always be getting all events. But due to the 124 | // selectiveness of the editor's InputOp, we can't fully rely on it, either. We need to combine the events of the 125 | // two. 126 | // 127 | // To be consistent, we handle all events after layout of the nested widgets, to have the same frame latency for all 128 | // events. 129 | handleKey := func(ev key.Event) { 130 | if ev.State == key.Press { 131 | firstVisible := w.list.Position.First 132 | lastVisible := w.list.Position.First + w.list.Position.Count - 1 133 | if w.list.Position.Offset > 0 { 134 | // The last element might be barely visible, even just one pixel. and we still want to scroll in that 135 | // case 136 | firstVisible++ 137 | } 138 | if w.list.Position.OffsetLast < 0 { 139 | // The last element might be barely visible, even just one pixel. and we still want to scroll in that 140 | // case 141 | lastVisible-- 142 | } 143 | visibleCount := lastVisible - firstVisible + 1 144 | 145 | switch ev.Name { 146 | case "↑": 147 | w.index-- 148 | if w.index < firstVisible { 149 | // XXX compute the correct position. the user might have scrolled the list via its scrollbar. 150 | w.list.Position.First-- 151 | } 152 | if w.index < 0 { 153 | w.index = len(w.filtered) - 1 154 | w.list.Position.First = w.index - visibleCount + 1 155 | } 156 | case "↓": 157 | w.index++ 158 | if w.index > lastVisible { 159 | // XXX compute the correct position. the user might have scrolled the list via its scrollbar. 160 | w.list.Position.First++ 161 | } 162 | if w.index >= len(w.filtered) { 163 | w.index = 0 164 | w.list.Position.First = 0 165 | w.list.Position.Offset = 0 166 | } 167 | case "⎋": // Escape 168 | w.cancelled = true 169 | } 170 | } 171 | } 172 | for _, evs := range spy.AllEvents() { 173 | for _, ev := range evs.Items { 174 | if ev, ok := ev.(key.Event); ok { 175 | handleKey(ev) 176 | } 177 | } 178 | } 179 | for _, ev := range w.input.Events() { 180 | switch ev.(type) { 181 | case widget.ChangeEvent: 182 | w.filtered = w.filtered[:0] 183 | f := w.BuildFilter(w.input.Text()) 184 | for _, item := range w.items { 185 | if f.Filter(item) { 186 | w.filtered = append(w.filtered, item.index) 187 | } 188 | } 189 | // TODO(dh): if the previously selected entry hasn't been filtered away, then it should stay selected. 190 | if w.index >= len(w.filtered) { 191 | w.index = len(w.filtered) - 1 192 | } 193 | if w.index < 0 && len(w.filtered) > 0 { 194 | w.index = 0 195 | } 196 | case widget.SubmitEvent: 197 | if len(w.filtered) != 0 { 198 | w.done = true 199 | } 200 | } 201 | } 202 | for i, idx := range w.filtered { 203 | if w.items[idx].click.Clicked(gtx) { 204 | w.index = i 205 | w.done = true 206 | } 207 | } 208 | 209 | for _, ev := range gtx.Events(w) { 210 | switch ev := ev.(type) { 211 | case key.Event: 212 | handleKey(ev) 213 | } 214 | } 215 | 216 | return dims 217 | } 218 | -------------------------------------------------------------------------------- /theme/modal.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "context" 5 | rtrace "runtime/trace" 6 | 7 | "honnef.co/go/gotraceui/color" 8 | "honnef.co/go/gotraceui/layout" 9 | 10 | "gioui.org/io/key" 11 | "gioui.org/io/pointer" 12 | "gioui.org/op/clip" 13 | ) 14 | 15 | type ModalStyle struct { 16 | Background color.Oklch 17 | Cancelled *bool 18 | } 19 | 20 | func Modal(cancelled *bool) ModalStyle { 21 | return ModalStyle{ 22 | Cancelled: cancelled, 23 | } 24 | } 25 | 26 | func (m ModalStyle) Layout(win *Window, gtx layout.Context, w Widget) layout.Dimensions { 27 | defer rtrace.StartRegion(context.Background(), "theme.Modal.Layout").End() 28 | 29 | // FIXME(dh): the modal doesn't cover the whole window if an offset or transform is active 30 | defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() 31 | Fill(win, gtx.Ops, m.Background) 32 | 33 | for _, ev := range gtx.Events(m) { 34 | switch ev := ev.(type) { 35 | case pointer.Event: 36 | if (ev.Priority == pointer.Foremost || ev.Priority == pointer.Grabbed) && ev.Kind == pointer.Press { 37 | *m.Cancelled = true 38 | } 39 | 40 | case key.Event: 41 | if ev.Name == "⎋" { 42 | *m.Cancelled = true 43 | } 44 | } 45 | } 46 | 47 | // TODO(dh): the tags should be pointers 48 | pointer.InputOp{Tag: m, Kinds: 0xFF}.Add(gtx.Ops) 49 | // TODO(dh): prevent all keyboard input from bubbling up 50 | // OPT(dh): using m as the tag allocates, because m is of type ModalStyle. 51 | key.InputOp{Tag: m, Keys: "A|B|C|D|E|F|G|H|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|⎋"}.Add(gtx.Ops) 52 | w(win, gtx) 53 | return layout.Dimensions{Size: gtx.Constraints.Max} 54 | } 55 | -------------------------------------------------------------------------------- /theme/panel.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "context" 5 | rtrace "runtime/trace" 6 | 7 | "honnef.co/go/gotraceui/color" 8 | "honnef.co/go/gotraceui/layout" 9 | "honnef.co/go/gotraceui/widget" 10 | ) 11 | 12 | type ComponentButtons struct { 13 | close widget.PrimaryClickable 14 | back widget.PrimaryClickable 15 | detach widget.PrimaryClickable 16 | attach widget.PrimaryClickable 17 | 18 | state ComponentState 19 | } 20 | 21 | func (pb *ComponentButtons) Transition(state ComponentState) { 22 | pb.state = state 23 | } 24 | 25 | func (pb *ComponentButtons) WantsTransition(gtx layout.Context) ComponentState { 26 | if pb.detach.Clicked(gtx) { 27 | return ComponentStateTab 28 | } else if pb.attach.Clicked(gtx) { 29 | return ComponentStatePanel 30 | } else if pb.close.Clicked(gtx) { 31 | return ComponentStateClosed 32 | } else { 33 | return ComponentStateNone 34 | } 35 | } 36 | 37 | func (pb *ComponentButtons) Backed(gtx layout.Context) bool { 38 | return pb.back.Clicked(gtx) 39 | } 40 | 41 | func (pb *ComponentButtons) Layout(win *Window, gtx layout.Context) layout.Dimensions { 42 | defer rtrace.StartRegion(context.Background(), "theme.ComponentButtons.Layout").End() 43 | 44 | type button struct { 45 | w *widget.PrimaryClickable 46 | label string 47 | cmd NormalCommand 48 | } 49 | 50 | var buttons []button 51 | switch pb.state { 52 | case ComponentStatePanel: 53 | buttons = []button{ 54 | { 55 | &pb.back, 56 | "Back", 57 | NormalCommand{ 58 | PrimaryLabel: "Go to previous panel", 59 | Aliases: []string{"back"}, 60 | }, 61 | }, 62 | 63 | { 64 | &pb.detach, 65 | "Tabify", 66 | NormalCommand{ 67 | PrimaryLabel: "Turn panel into tab", 68 | }, 69 | }, 70 | } 71 | case ComponentStateTab: 72 | case ComponentStateWindow: 73 | buttons = []button{ 74 | { 75 | &pb.attach, 76 | "Attach", 77 | NormalCommand{ 78 | PrimaryLabel: "Attach panel", 79 | }, 80 | }, 81 | 82 | { 83 | &pb.close, 84 | "Close", 85 | NormalCommand{ 86 | PrimaryLabel: "Close panel", 87 | }, 88 | }, 89 | } 90 | } 91 | 92 | var cmds CommandSlice 93 | children := make([]layout.Widget, 0, 3) 94 | for _, btn := range buttons { 95 | btn := btn 96 | children = append(children, 97 | func(gtx layout.Context) layout.Dimensions { 98 | return Button(win.Theme, &btn.w.Clickable, btn.label).Layout(win, gtx) 99 | }, 100 | layout.Spacer{Width: 5}.Layout, 101 | ) 102 | 103 | cmd := btn.cmd 104 | cmd.Category = "Panel" 105 | cmd.Color = color.Oklch{L: 0.7862, C: 0.104, H: 140, A: 1} 106 | cmd.Fn = func() Action { 107 | return ExecuteAction(func(gtx layout.Context) { 108 | btn.w.Click() 109 | }) 110 | } 111 | cmds = append(cmds, cmd) 112 | } 113 | 114 | win.AddCommandProvider(CommandSlice(cmds)) 115 | return layout.Rigids(gtx, layout.Horizontal, children...) 116 | } 117 | 118 | // WidgetComponent turns any widget into a Component. You are responsible for handling component button events. 119 | type WidgetComponent struct { 120 | w Widget 121 | ComponentButtons 122 | } 123 | 124 | func (wp *WidgetComponent) Layout(win *Window, gtx layout.Context) layout.Dimensions { 125 | defer rtrace.StartRegion(context.Background(), "theme.WidgetComponent.Layout").End() 126 | 127 | return layout.Rigids(gtx, layout.Horizontal, Dumb(win, wp.ComponentButtons.Layout), Dumb(win, wp.w)) 128 | } 129 | -------------------------------------------------------------------------------- /theme/scrollbar.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "context" 5 | "image" 6 | "math" 7 | rtrace "runtime/trace" 8 | 9 | "honnef.co/go/gotraceui/color" 10 | "honnef.co/go/gotraceui/layout" 11 | "honnef.co/go/gotraceui/widget" 12 | 13 | "gioui.org/io/pointer" 14 | "gioui.org/op" 15 | "gioui.org/op/clip" 16 | "gioui.org/unit" 17 | ) 18 | 19 | // rangeIsScrollable returns whether the viewport described by start and end 20 | // is smaller than the underlying content (such that it can be scrolled). 21 | // start and end are expected to each be in the range [0,1], and start 22 | // must be less than or equal to end. 23 | func rangeIsScrollable(start, end float32) bool { 24 | return end-start < 1 25 | } 26 | 27 | // ScrollTrackStyle configures the presentation of a track for a scroll area. 28 | type ScrollTrackStyle struct { 29 | // MajorPadding and MinorPadding along the major and minor axis of the 30 | // scrollbar's track. This is used to keep the scrollbar from touching 31 | // the edges of the content area. 32 | MinorPadding unit.Dp 33 | // Color of the track background. 34 | Color color.Oklch 35 | } 36 | 37 | // ScrollIndicatorStyle configures the presentation of a scroll indicator. 38 | type ScrollIndicatorStyle struct { 39 | // MajorMinLen is the smallest that the scroll indicator is allowed to 40 | // be along the major axis. 41 | MajorMinLen unit.Dp 42 | // MinorWidth is the width of the scroll indicator across the minor axis. 43 | MinorWidth unit.Dp 44 | // Color and HoverColor are the normal and hovered colors of the scroll 45 | // indicator. 46 | Color, HoverColor color.Oklch 47 | } 48 | 49 | // ScrollbarStyle configures the presentation of a scrollbar. 50 | type ScrollbarStyle struct { 51 | Scrollbar *widget.Scrollbar 52 | Track ScrollTrackStyle 53 | Indicator ScrollIndicatorStyle 54 | } 55 | 56 | // Scrollbar configures the presentation of a scrollbar using the provided 57 | // theme and state. 58 | func Scrollbar(th *Theme, state *widget.Scrollbar) ScrollbarStyle { 59 | return ScrollbarStyle{ 60 | Scrollbar: state, 61 | Track: ScrollTrackStyle{ 62 | MinorPadding: 2, 63 | // TODO(dh): compute color from background color 64 | Color: oklch(66.61, 0.1, 108.83), 65 | }, 66 | Indicator: ScrollIndicatorStyle{ 67 | MajorMinLen: 8, 68 | MinorWidth: 10, 69 | Color: th.Palette.Background, 70 | HoverColor: th.Palette.Background, 71 | }, 72 | } 73 | } 74 | 75 | // Width returns the minor axis width of the scrollbar in its current 76 | // configuration (taking padding for the scroll track into account). 77 | func (s ScrollbarStyle) Width() unit.Dp { 78 | return s.Indicator.MinorWidth + s.Track.MinorPadding + s.Track.MinorPadding 79 | } 80 | 81 | // Layout the scrollbar. 82 | func (s ScrollbarStyle) Layout(win *Window, gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) layout.Dimensions { 83 | defer rtrace.StartRegion(context.Background(), "theme.ScrollbarStyle.Layout").End() 84 | defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() 85 | 86 | // Set minimum constraints in an axis-independent way, then convert to 87 | // the correct representation for the current axis. 88 | convert := axis.Convert 89 | maxMajorAxis := convert(gtx.Constraints.Max).X 90 | gtx.Constraints.Min.X = maxMajorAxis 91 | gtx.Constraints.Min.Y = gtx.Dp(s.Width()) 92 | gtx.Constraints.Min = convert(gtx.Constraints.Min) 93 | gtx.Constraints.Max = gtx.Constraints.Min 94 | 95 | if !rangeIsScrollable(viewportStart, viewportEnd) { 96 | return layout.Dimensions{Size: gtx.Constraints.Min} 97 | } 98 | 99 | s.Scrollbar.Layout(gtx, axis, viewportStart, viewportEnd) 100 | 101 | // Darken indicator if hovered. 102 | if s.Scrollbar.IndicatorHovered() { 103 | s.Indicator.Color = s.Indicator.HoverColor 104 | } 105 | 106 | return s.layout(win, gtx, axis, viewportStart, viewportEnd) 107 | } 108 | 109 | // layout the scroll track and indicator. 110 | func (s ScrollbarStyle) layout(win *Window, gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) layout.Dimensions { 111 | // XXX there's an off-by-some somewhere, and padding on the right side is larger than on the left 112 | 113 | inset := layout.Inset{ 114 | Top: 0, 115 | Bottom: 0, 116 | Left: s.Track.MinorPadding, 117 | Right: s.Track.MinorPadding, 118 | } 119 | if axis == layout.Horizontal { 120 | inset.Top, inset.Bottom, inset.Left, inset.Right = inset.Left, inset.Right, inset.Top, inset.Bottom 121 | } 122 | // Capture the outer constraints because layout.Stack will reset 123 | // the minimum to zero. 124 | outerConstraints := gtx.Constraints 125 | 126 | return layout.Stack{}.Layout(gtx, 127 | layout.Expanded(func(gtx layout.Context) layout.Dimensions { 128 | // Lay out the draggable track underneath the scroll indicator. 129 | area := image.Rectangle{ 130 | Max: gtx.Constraints.Min, 131 | } 132 | pointerArea := clip.Rect(area) 133 | defer pointerArea.Push(gtx.Ops).Pop() 134 | s.Scrollbar.AddDrag(gtx.Ops) 135 | 136 | // Stack a normal clickable area on top of the draggable area 137 | // to capture non-dragging clicks. 138 | defer pointer.PassOp{}.Push(gtx.Ops).Pop() 139 | defer pointerArea.Push(gtx.Ops).Pop() 140 | s.Scrollbar.AddTrack(gtx.Ops) 141 | 142 | FillShape(win, gtx.Ops, s.Track.Color, clip.Rect(area).Op()) 143 | return layout.Dimensions{} 144 | }), 145 | layout.Stacked(func(gtx layout.Context) layout.Dimensions { 146 | gtx.Constraints = outerConstraints 147 | return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 148 | // Use axis-independent constraints. 149 | gtx.Constraints.Min = axis.Convert(gtx.Constraints.Min) 150 | gtx.Constraints.Max = axis.Convert(gtx.Constraints.Max) 151 | 152 | // Compute the pixel size and position of the scroll indicator within 153 | // the track. 154 | trackLen := gtx.Constraints.Min.X 155 | viewStart := int(math.Round(float64(viewportStart) * float64(trackLen))) 156 | viewEnd := int(math.Round(float64(viewportEnd) * float64(trackLen))) 157 | indicatorLen := max(viewEnd-viewStart, gtx.Dp(s.Indicator.MajorMinLen)) 158 | if viewStart+indicatorLen > trackLen { 159 | viewStart = trackLen - indicatorLen 160 | } 161 | indicatorDims := axis.Convert(image.Point{ 162 | X: indicatorLen, 163 | Y: gtx.Dp(s.Indicator.MinorWidth), 164 | }) 165 | 166 | // Lay out the indicator. 167 | offset := axis.Convert(image.Pt(viewStart, 0)) 168 | defer op.Offset(offset).Push(gtx.Ops).Pop() 169 | FillShape(win, gtx.Ops, s.Indicator.Color, clip.Rect{Max: indicatorDims}.Op()) 170 | 171 | // Add the indicator pointer hit area. 172 | area := clip.Rect(image.Rectangle{Max: indicatorDims}) 173 | defer pointer.PassOp{}.Push(gtx.Ops).Pop() 174 | defer area.Push(gtx.Ops).Pop() 175 | s.Scrollbar.AddIndicator(gtx.Ops) 176 | 177 | return layout.Dimensions{Size: axis.Convert(gtx.Constraints.Min)} 178 | }) 179 | }), 180 | ) 181 | } 182 | -------------------------------------------------------------------------------- /theme/util.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "context" 5 | "image" 6 | rtrace "runtime/trace" 7 | 8 | "honnef.co/go/gotraceui/color" 9 | "honnef.co/go/gotraceui/layout" 10 | 11 | "gioui.org/op" 12 | "gioui.org/op/clip" 13 | "gioui.org/op/paint" 14 | "gioui.org/unit" 15 | ) 16 | 17 | // TODO(dh): Bordered, Border, and TextLine probably belong in the theme package instead. 18 | 19 | // Bordered is like Border, but automatically applys an inset. 20 | type Bordered struct { 21 | Color color.Oklch 22 | Width unit.Dp 23 | } 24 | 25 | func (b Bordered) Layout(win *Window, gtx layout.Context, w Widget) layout.Dimensions { 26 | return Border(b).Layout(win, gtx, func(win *Window, gtx layout.Context) layout.Dimensions { 27 | return layout.UniformInset(b.Width).Layout(gtx, Dumb(win, w)) 28 | }) 29 | } 30 | 31 | // Border draws a border and renders a widget inside it. 32 | type Border struct { 33 | Color color.Oklch 34 | Width unit.Dp 35 | } 36 | 37 | func (b Border) Layout(win *Window, gtx layout.Context, w Widget) layout.Dimensions { 38 | defer rtrace.StartRegion(context.Background(), "widget.Border.Layout").End() 39 | 40 | dims := w(win, gtx) 41 | sz := dims.Size 42 | bwidth := gtx.Dp(b.Width) 43 | 44 | nwne := clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(sz.X, bwidth)}.Op() 45 | nese := clip.Rect{Min: image.Pt(sz.X-bwidth, bwidth), Max: image.Pt(sz.X, sz.Y-bwidth)}.Op() 46 | nwsw := clip.Rect{Min: image.Pt(0, bwidth), Max: image.Pt(bwidth, sz.Y)}.Op() 47 | swse := clip.Rect{Min: image.Pt(bwidth, sz.Y-bwidth), Max: image.Pt(sz.X, sz.Y)}.Op() 48 | 49 | FillShape(win, gtx.Ops, b.Color, nwne) 50 | FillShape(win, gtx.Ops, b.Color, nese) 51 | FillShape(win, gtx.Ops, b.Color, nwsw) 52 | FillShape(win, gtx.Ops, b.Color, swse) 53 | 54 | return dims 55 | } 56 | 57 | type Background struct { 58 | Color color.Oklch 59 | } 60 | 61 | func (b Background) Layout(win *Window, gtx layout.Context, w Widget) layout.Dimensions { 62 | defer rtrace.StartRegion(context.Background(), "widget.Background.Layout").End() 63 | 64 | macro := op.Record(gtx.Ops) 65 | dims := w(win, gtx) 66 | call := macro.Stop() 67 | 68 | FillShape(win, gtx.Ops, b.Color, clip.Rect{Max: dims.Size}.Op()) 69 | call.Add(gtx.Ops) 70 | return dims 71 | } 72 | 73 | // FillShape fills the clip shape with a color. 74 | func FillShape(win *Window, ops *op.Ops, c color.Oklch, shape clip.Op) { 75 | defer shape.Push(ops).Pop() 76 | Fill(win, ops, c) 77 | } 78 | 79 | // Fill paints an infinitely large plane with the provided color. It 80 | // is intended to be used with a clip.Op already in place to limit 81 | // the painted area. Use FillShape unless you need to paint several 82 | // times within the same clip.Op. 83 | func Fill(win *Window, ops *op.Ops, c color.Oklch) { 84 | paint.ColorOp{Color: win.ConvertColor(c)}.Add(ops) 85 | paint.PaintOp{}.Add(ops) 86 | } 87 | -------------------------------------------------------------------------------- /tinylfu/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Damian Gryski 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 | -------------------------------------------------------------------------------- /tinylfu/cm4.go: -------------------------------------------------------------------------------- 1 | package tinylfu 2 | 3 | // cm4 is a small conservative-update count-min sketch implementation with 4-bit counters 4 | type cm4 struct { 5 | s [depth]nvec 6 | mask uint32 7 | } 8 | 9 | const depth = 4 10 | 11 | func newCM4(w int) *cm4 { 12 | if w < 1 { 13 | panic("cm4: bad width") 14 | } 15 | 16 | // use 4 counters per item per level, for a total of 16 counters or 8 bytes per item, matching the TinyLFU paper. 17 | w32 := nextPowerOfTwo(uint32(w) * 4) 18 | c := cm4{ 19 | mask: w32 - 1, 20 | } 21 | 22 | for i := 0; i < depth; i++ { 23 | c.s[i] = newNvec(int(w32)) 24 | } 25 | 26 | return &c 27 | } 28 | 29 | func (c *cm4) add(keyh uint64) { 30 | // The loop unrolling prevents this function from being inlined, but it still results in a slight overall speedup. 31 | c.s[3].inc(c.counterOffset(keyh, 3)) 32 | c.s[2].inc(c.counterOffset(keyh, 2)) 33 | c.s[1].inc(c.counterOffset(keyh, 1)) 34 | c.s[0].inc(c.counterOffset(keyh, 0)) 35 | } 36 | 37 | func (c *cm4) counterOffset(keyh uint64, level int) uint32 { 38 | // counterOffset gets inlined and the compiler removes the duplicated computations of h1 and h2, so there is no 39 | // benefit to accepting h1 and h2 as arguments. 40 | h1, h2 := uint32(keyh), uint32(keyh>>32) 41 | return (h1 + uint32(level)*h2) & c.mask 42 | } 43 | 44 | func (c *cm4) estimate(keyh uint64) byte { 45 | var min byte = 255 46 | bmin := func(v, min byte) byte { 47 | // bmin gets inlined and the else branch gets optimized away 48 | if v < min { 49 | return v 50 | } else { 51 | return min 52 | } 53 | } 54 | min = bmin(c.s[3].get(c.counterOffset(keyh, 3)), min) 55 | min = bmin(c.s[2].get(c.counterOffset(keyh, 2)), min) 56 | min = bmin(c.s[1].get(c.counterOffset(keyh, 1)), min) 57 | min = bmin(c.s[0].get(c.counterOffset(keyh, 0)), min) 58 | return min 59 | } 60 | 61 | func (c *cm4) reset() { 62 | // There is no point in unrolling this loop, the cost is dominated by nvec.reset, which is O(n) 63 | for _, n := range c.s { 64 | n.reset() 65 | } 66 | } 67 | 68 | // nybble vector 69 | type nvec []byte 70 | 71 | func newNvec(w int) nvec { 72 | return make(nvec, w/2) 73 | } 74 | 75 | func (n nvec) get(i uint32) byte { 76 | // Ugly, but as a single expression so the compiler will inline it :/ 77 | return byte(n[i/2]>>((i&1)*4)) & 0x0f 78 | } 79 | 80 | func (n nvec) inc(i uint32) { 81 | idx := i / 2 82 | shift := (i & 1) * 4 83 | v := (n[idx] >> shift) & 0x0f 84 | if v < 15 { 85 | n[idx] += 1 << shift 86 | } 87 | } 88 | 89 | func (n nvec) reset() { 90 | for i := range n { 91 | n[i] = (n[i] >> 1) & 0x77 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tinylfu/cm4_test.go: -------------------------------------------------------------------------------- 1 | package tinylfu 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNvec(t *testing.T) { 8 | 9 | n := newNvec(8) 10 | 11 | n.inc(0) 12 | if n[0] != 0x01 { 13 | t.Errorf("n[0]=0x%02x, want 0x01: (n=% 02x)", n[0], n) 14 | } 15 | if w := n.get(0); w != 1 { 16 | t.Errorf("n.get(0)=%d, want 1", w) 17 | } 18 | if w := n.get(1); w != 0 { 19 | t.Errorf("n.get(1)=%d, want 0", w) 20 | } 21 | 22 | n.inc(1) 23 | if n[0] != 0x11 { 24 | t.Errorf("n[0]=0x%02x, want 0x11: (n=% 02x)", n[0], n) 25 | } 26 | if w := n.get(0); w != 1 { 27 | t.Errorf("n.get(0)=%d, want 1", w) 28 | } 29 | if w := n.get(1); w != 1 { 30 | t.Errorf("n.get(1)=%d, want 1", w) 31 | } 32 | 33 | for i := 0; i < 14; i++ { 34 | n.inc(1) 35 | } 36 | if n[0] != 0xf1 { 37 | t.Errorf("n[1]=0x%02x, want 0xf1: (n=% 02x)", n[0], n) 38 | } 39 | if w := n.get(1); w != 15 { 40 | t.Errorf("n.get(1)=%d, want 15", w) 41 | } 42 | if w := n.get(0); w != 1 { 43 | t.Errorf("n.get(0)=%d, want 1", w) 44 | } 45 | 46 | // ensure clamped 47 | for i := 0; i < 3; i++ { 48 | n.inc(1) 49 | if n[0] != 0xf1 { 50 | t.Errorf("n[0]=0x%02x, want 0xf1: (n=% 02x)", n[0], n) 51 | } 52 | } 53 | 54 | n.reset() 55 | 56 | if n[0] != 0x70 { 57 | t.Errorf("n[0]=0x%02x, want 0x70 (n=% 02x)", n[0], n) 58 | } 59 | } 60 | 61 | func TestCM4(t *testing.T) { 62 | 63 | cm := newCM4(32) 64 | 65 | hash := uint64(0x0ddc0ffeebadf00d) 66 | 67 | cm.add(hash) 68 | cm.add(hash) 69 | 70 | if got := cm.estimate(hash); got != 2 { 71 | t.Errorf("cm.estimate(%x)=%d, want 2\n", hash, got) 72 | } 73 | } 74 | 75 | func BenchmarkCMAddSaturated(b *testing.B) { 76 | cm := newCM4(32) 77 | hash := uint64(0x0ddc0ffeebadf00d) 78 | for i := 0; i < b.N; i++ { 79 | cm.add(hash) 80 | } 81 | } 82 | 83 | var SinkByte byte 84 | 85 | func BenchmarkCMEstimate(b *testing.B) { 86 | cm := newCM4(32) 87 | hash := uint64(0x0ddc0ffeebadf00d) 88 | cm.add(hash) 89 | for i := 0; i < b.N; i++ { 90 | SinkByte = cm.estimate(hash) 91 | } 92 | } 93 | 94 | func BenchmarkCMReset(b *testing.B) { 95 | cm := newCM4(3200) 96 | for i := 0; i < b.N; i++ { 97 | cm.reset() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tinylfu/doorkeeper.go: -------------------------------------------------------------------------------- 1 | package tinylfu 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // doorkeeper is a small bloom-filter-based cache admission policy 8 | type doorkeeper struct { 9 | m uint32 // size of bit vector in bits 10 | k uint32 // distinct hash functions needed 11 | filter bitvector // our filter bit vector 12 | } 13 | 14 | func newDoorkeeper(capacity int, falsePositiveRate float64) *doorkeeper { 15 | bits := float64(capacity) * -math.Log(falsePositiveRate) / (math.Log(2.0) * math.Log(2.0)) // in bits 16 | m := nextPowerOfTwo(uint32(bits)) 17 | 18 | if m < 1024 { 19 | m = 1024 20 | } 21 | 22 | k := uint32(0.7 * float64(m) / float64(capacity)) 23 | if k < 2 { 24 | k = 2 25 | } 26 | 27 | return &doorkeeper{ 28 | m: m, 29 | filter: newbv(m), 30 | k: k, 31 | } 32 | } 33 | 34 | func (d *doorkeeper) allow(keyh uint64) bool { 35 | if d == nil { 36 | return true 37 | } 38 | alreadyPresent := d.insert(keyh) 39 | return alreadyPresent 40 | } 41 | 42 | // insert inserts the byte array b into the bloom filter. Returns true if the value 43 | // was already considered to be in the bloom filter. 44 | func (d *doorkeeper) insert(h uint64) bool { 45 | h1, h2 := uint32(h), uint32(h>>32) 46 | var o uint = 1 47 | for i := uint32(0); i < d.k; i++ { 48 | o &= d.filter.getset((h1 + i*h2) & (d.m - 1)) 49 | } 50 | return o == 1 51 | } 52 | 53 | // Reset clears the bloom filter 54 | func (d *doorkeeper) reset() { 55 | if d == nil { 56 | return 57 | } 58 | for i := range d.filter { 59 | d.filter[i] = 0 60 | } 61 | } 62 | 63 | // Internal routines for the bit vector 64 | type bitvector []uint64 65 | 66 | func newbv(size uint32) bitvector { 67 | return make([]uint64, uint(size+63)/64) 68 | } 69 | 70 | // set bit 'bit' in the bitvector d and return previous value 71 | func (b bitvector) getset(bit uint32) uint { 72 | shift := bit % 64 73 | idx := bit / 64 74 | bb := b[idx] 75 | m := uint64(1) << shift 76 | b[idx] |= m 77 | return uint((bb & m) >> shift) 78 | } 79 | 80 | // return the integer >= i which is a power of two 81 | func nextPowerOfTwo(i uint32) uint32 { 82 | n := i - 1 83 | n |= n >> 1 84 | n |= n >> 2 85 | n |= n >> 4 86 | n |= n >> 8 87 | n |= n >> 16 88 | n++ 89 | return n 90 | } 91 | -------------------------------------------------------------------------------- /tinylfu/lru.go: -------------------------------------------------------------------------------- 1 | package tinylfu 2 | 3 | import "honnef.co/go/gotraceui/tinylfu/internal/list" 4 | 5 | // Cache is an LRU cache. It is not safe for concurrent access. 6 | type lruCache[K comparable, V any] struct { 7 | data map[K]*list.Element[*slruItem[K, V]] 8 | cap int 9 | ll *list.List[*slruItem[K, V]] 10 | } 11 | 12 | func newLRU[K comparable, V any](cap int, data map[K]*list.Element[*slruItem[K, V]]) *lruCache[K, V] { 13 | return &lruCache[K, V]{ 14 | data: data, 15 | cap: cap, 16 | ll: list.New[*slruItem[K, V]](), 17 | } 18 | } 19 | 20 | // Get returns a value from the cache 21 | func (lru *lruCache[K, V]) get(v *list.Element[*slruItem[K, V]]) { 22 | lru.ll.MoveToFront(v) 23 | } 24 | 25 | // Set sets a value in the cache 26 | func (lru *lruCache[K, V]) add(newitem slruItem[K, V]) (oitem slruItem[K, V], evicted bool) { 27 | if lru.ll.Len() < lru.cap { 28 | lru.data[newitem.key] = lru.ll.PushFront(&newitem) 29 | return slruItem[K, V]{}, false 30 | } 31 | 32 | // reuse the tail item 33 | e := lru.ll.Back() 34 | item := e.Value 35 | 36 | delete(lru.data, item.key) 37 | 38 | oitem = *item 39 | *item = newitem 40 | 41 | lru.data[item.key] = e 42 | lru.ll.MoveToFront(e) 43 | 44 | return oitem, true 45 | } 46 | 47 | // Len returns the total number of items in the cache 48 | func (lru *lruCache[K, V]) Len() int { 49 | return len(lru.data) 50 | } 51 | 52 | // Remove removes an item from the cache, returning the item and a boolean indicating if it was found 53 | func (lru *lruCache[K, V]) Remove(key K) (V, bool) { 54 | v, ok := lru.data[key] 55 | if !ok { 56 | return *new(V), false 57 | } 58 | item := v.Value 59 | lru.ll.Remove(v) 60 | delete(lru.data, key) 61 | return item.value, true 62 | } 63 | -------------------------------------------------------------------------------- /tinylfu/s2lru.go: -------------------------------------------------------------------------------- 1 | package tinylfu 2 | 3 | import "honnef.co/go/gotraceui/tinylfu/internal/list" 4 | 5 | type slruItem[K comparable, V any] struct { 6 | listid int 7 | key K 8 | value V 9 | keyh uint64 10 | } 11 | 12 | // Cache is an LRU cache. It is not safe for concurrent access. 13 | type slruCache[K comparable, V any] struct { 14 | data map[K]*list.Element[*slruItem[K, V]] 15 | onecap, twocap int 16 | one, two *list.List[*slruItem[K, V]] 17 | } 18 | 19 | func newSLRU[K comparable, V any](onecap, twocap int, data map[K]*list.Element[*slruItem[K, V]]) *slruCache[K, V] { 20 | return &slruCache[K, V]{ 21 | data: data, 22 | onecap: onecap, 23 | one: list.New[*slruItem[K, V]](), 24 | twocap: twocap, 25 | two: list.New[*slruItem[K, V]](), 26 | } 27 | } 28 | 29 | // get updates the cache data structures for a get 30 | func (slru *slruCache[K, V]) get(v *list.Element[*slruItem[K, V]]) { 31 | item := v.Value 32 | 33 | // already on list two? 34 | if item.listid == 2 { 35 | slru.two.MoveToFront(v) 36 | return 37 | } 38 | 39 | // must be list one 40 | 41 | // is there space on the next list? 42 | if slru.two.Len() < slru.twocap { 43 | // just do the remove/add 44 | slru.one.Remove(v) 45 | item.listid = 2 46 | slru.data[item.key] = slru.two.PushFront(item) 47 | return 48 | } 49 | 50 | back := slru.two.Back() 51 | bitem := back.Value 52 | 53 | // swap the key/values 54 | *bitem, *item = *item, *bitem 55 | 56 | bitem.listid = 2 57 | item.listid = 1 58 | 59 | // update pointers in the map 60 | slru.data[item.key] = v 61 | slru.data[bitem.key] = back 62 | 63 | // move the elements to the front of their lists 64 | slru.one.MoveToFront(v) 65 | slru.two.MoveToFront(back) 66 | } 67 | 68 | // Set sets a value in the cache 69 | func (slru *slruCache[K, V]) add(newitem slruItem[K, V]) { 70 | 71 | newitem.listid = 1 72 | 73 | if slru.one.Len() < slru.onecap || slru.Len() < slru.onecap+slru.twocap { 74 | slru.data[newitem.key] = slru.one.PushFront(&newitem) 75 | return 76 | } 77 | 78 | // reuse the tail item 79 | e := slru.one.Back() 80 | item := e.Value 81 | 82 | delete(slru.data, item.key) 83 | 84 | *item = newitem 85 | 86 | slru.data[item.key] = e 87 | slru.one.MoveToFront(e) 88 | } 89 | 90 | func (slru *slruCache[K, V]) victim() *slruItem[K, V] { 91 | 92 | if slru.Len() < slru.onecap+slru.twocap { 93 | return nil 94 | } 95 | 96 | v := slru.one.Back() 97 | 98 | return v.Value 99 | } 100 | 101 | // Len returns the total number of items in the cache 102 | func (slru *slruCache[K, V]) Len() int { 103 | return slru.one.Len() + slru.two.Len() 104 | } 105 | 106 | // Remove removes an item from the cache, returning the item and a boolean indicating if it was found 107 | func (slru *slruCache[K, V]) Remove(key K) (V, bool) { 108 | v, ok := slru.data[key] 109 | if !ok { 110 | return *new(V), false 111 | } 112 | 113 | item := v.Value 114 | 115 | if item.listid == 2 { 116 | slru.two.Remove(v) 117 | } else { 118 | slru.one.Remove(v) 119 | } 120 | 121 | delete(slru.data, key) 122 | 123 | return item.value, true 124 | } 125 | -------------------------------------------------------------------------------- /tinylfu/tinylfu.go: -------------------------------------------------------------------------------- 1 | // Package tinylfu is an implementation of the TinyLFU caching algorithm 2 | /* 3 | http://arxiv.org/abs/1512.00727 4 | */ 5 | package tinylfu 6 | 7 | import ( 8 | "hash/maphash" 9 | "unsafe" 10 | 11 | "honnef.co/go/gotraceui/tinylfu/internal/list" 12 | ) 13 | 14 | type T[K comparable, V any] struct { 15 | c *cm4 16 | bouncer *doorkeeper 17 | w int 18 | samples int 19 | lru *lruCache[K, V] 20 | slru *slruCache[K, V] 21 | data map[K]*list.Element[*slruItem[K, V]] 22 | seed maphash.Seed 23 | } 24 | 25 | func New[K comparable, V any](size int, samples int) *T[K, V] { 26 | 27 | const lruPct = 1 28 | 29 | lruSize := (lruPct * size) / 100 30 | if lruSize < 1 { 31 | lruSize = 1 32 | } 33 | slruSize := size - lruSize 34 | if slruSize < 1 { 35 | slruSize = 1 36 | 37 | } 38 | slru20 := slruSize / 5 39 | if slru20 < 1 { 40 | slru20 = 1 41 | } 42 | 43 | data := make(map[K]*list.Element[*slruItem[K, V]], size) 44 | 45 | return &T[K, V]{ 46 | c: newCM4(size), 47 | w: 0, 48 | samples: samples, 49 | bouncer: newDoorkeeper(samples, 0.01), 50 | 51 | data: data, 52 | 53 | lru: newLRU(lruSize, data), 54 | slru: newSLRU(slru20, slruSize-slru20, data), 55 | 56 | seed: maphash.MakeSeed(), 57 | } 58 | } 59 | 60 | func (t *T[K, V]) Get(key K) (V, bool) { 61 | 62 | t.w++ 63 | if t.w == t.samples { 64 | t.c.reset() 65 | t.bouncer.reset() 66 | t.w = 0 67 | } 68 | 69 | val, ok := t.data[key] 70 | if !ok { 71 | var akey string 72 | switch ikey := any(key).(type) { 73 | case string: 74 | akey = ikey 75 | default: 76 | akey = unsafe.String((*byte)(unsafe.Pointer(&key)), unsafe.Sizeof(key)) 77 | } 78 | keyh := maphash.String(t.seed, akey) 79 | t.c.add(keyh) 80 | return *new(V), false 81 | } 82 | 83 | item := val.Value 84 | 85 | t.c.add(item.keyh) 86 | 87 | v := item.value 88 | if item.listid == 0 { 89 | t.lru.get(val) 90 | } else { 91 | t.slru.get(val) 92 | } 93 | 94 | return v, true 95 | } 96 | 97 | func (t *T[K, V]) Add(key K, val V) { 98 | 99 | if e, ok := t.data[key]; ok { 100 | // Key is already in our cache. 101 | // `Add` will act as a `Get` for list movements 102 | item := e.Value 103 | item.value = val 104 | t.c.add(item.keyh) 105 | 106 | if item.listid == 0 { 107 | t.lru.get(e) 108 | } else { 109 | t.slru.get(e) 110 | } 111 | return 112 | } 113 | 114 | var akey string 115 | switch ikey := any(key).(type) { 116 | case string: 117 | akey = ikey 118 | default: 119 | akey = unsafe.String((*byte)(unsafe.Pointer(&key)), unsafe.Sizeof(key)) 120 | } 121 | newitem := slruItem[K, V]{0, key, val, maphash.String(t.seed, akey)} 122 | 123 | oitem, evicted := t.lru.add(newitem) 124 | if !evicted { 125 | return 126 | } 127 | 128 | // estimate count of what will be evicted from slru 129 | victim := t.slru.victim() 130 | if victim == nil { 131 | t.slru.add(oitem) 132 | return 133 | } 134 | 135 | if !t.bouncer.allow(oitem.keyh) { 136 | return 137 | } 138 | 139 | vcount := t.c.estimate(victim.keyh) 140 | ocount := t.c.estimate(oitem.keyh) 141 | 142 | if ocount < vcount { 143 | return 144 | } 145 | 146 | t.slru.add(oitem) 147 | } 148 | -------------------------------------------------------------------------------- /tinylfu/tinylfu_test.go: -------------------------------------------------------------------------------- 1 | package tinylfu 2 | 3 | import "testing" 4 | 5 | func TestAddAlreadyInCache(t *testing.T) { 6 | c := New[string, string](100, 10000) 7 | 8 | c.Add("foo", "bar") 9 | 10 | val, _ := c.Get("foo") 11 | if val != "bar" { 12 | t.Errorf("c.Get(foo)=%q, want %q", val, "bar") 13 | } 14 | 15 | c.Add("foo", "baz") 16 | 17 | val, _ = c.Get("foo") 18 | if val != "baz" { 19 | t.Errorf("c.Get(foo)=%q, want %q", val, "baz") 20 | } 21 | } 22 | 23 | var SinkString string 24 | var SinkBool bool 25 | 26 | func BenchmarkGet(b *testing.B) { 27 | t := New[string, string](64, 640) 28 | key := "some arbitrary key" 29 | val := "some arbitrary value" 30 | t.Add(key, val) 31 | for i := 0; i < b.N; i++ { 32 | SinkString, SinkBool = t.Get(key) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /trace/ptrace/debug.go: -------------------------------------------------------------------------------- 1 | //go:build debug 2 | 3 | package ptrace 4 | 5 | const debug = true 6 | -------------------------------------------------------------------------------- /trace/ptrace/nodebug.go: -------------------------------------------------------------------------------- 1 | //go:build !debug 2 | 3 | package ptrace 4 | 5 | const debug = false 6 | -------------------------------------------------------------------------------- /trace/ptrace/pattern.go: -------------------------------------------------------------------------------- 1 | package ptrace 2 | 3 | type SpanTags uint8 4 | 5 | const ( 6 | SpanTagNetwork SpanTags = 1 << iota 7 | SpanTagTCP 8 | SpanTagTLS 9 | SpanTagRead 10 | SpanTagAccept 11 | SpanTagDial 12 | SpanTagHTTP 13 | 14 | // Used for spans of GC goroutines, used when choosing span colors for processor timelines. 15 | SpanTagGC 16 | ) 17 | 18 | type pattern struct { 19 | state SchedulingState 20 | // fns looks for functions in the stack at absolute offsets 21 | fns []string 22 | 23 | // relFns looks for runs of functions in the stack, at no particular offsets 24 | relFns [][]string 25 | 26 | newState SchedulingState 27 | at uint8 28 | tags SpanTags 29 | } 30 | 31 | // TODO(dh): implement a pattern language similar to that used in Staticcheck so that we can express ANDs and ORs 32 | // without having to write a bunch of Go. 33 | var patterns = [256][]pattern{ 34 | // XXX add a pattern for "GC incremental sweep" range, to skip some amount of frames 35 | StateBlocked: { 36 | { 37 | state: StateBlocked, 38 | fns: []string{ 39 | 0: "runtime.ReadTrace", 40 | }, 41 | newState: StateInactive, 42 | }, 43 | }, 44 | 45 | StateBlockedRecv: { 46 | { 47 | state: StateBlockedRecv, 48 | fns: []string{ 49 | 0: "runtime.chanrecv1", 50 | }, 51 | at: 1, 52 | }, 53 | { 54 | state: StateBlockedRecv, 55 | fns: []string{ 56 | 0: "runtime.chanrecv2", 57 | }, 58 | at: 1, 59 | }, 60 | }, 61 | 62 | StateBlockedSend: { 63 | { 64 | state: StateBlockedSend, 65 | fns: []string{ 66 | 0: "runtime.chansend1", 67 | }, 68 | at: 1, 69 | }, 70 | }, 71 | 72 | StateBlockedSync: { 73 | { 74 | state: StateBlockedSync, 75 | fns: []string{ 76 | 0: "runtime.gcStart", 77 | }, 78 | newState: StateBlockedSyncTriggeringGC, 79 | }, 80 | { 81 | state: StateBlockedSync, 82 | fns: []string{ 83 | 0: "sync.(*Mutex).Lock", 84 | 1: "sync.(*Once).doSlow", 85 | 2: "sync.(*Once).Do", 86 | }, 87 | newState: StateBlockedSyncOnce, 88 | at: 3, 89 | }, 90 | }, 91 | 92 | StateBlockedCond: { 93 | { 94 | state: StateBlockedCond, 95 | fns: []string{ 96 | 0: "sync.(*Cond).Wait", 97 | }, 98 | at: 1, 99 | }, 100 | }, 101 | 102 | StateBlockedNet: { 103 | { 104 | state: StateBlockedNet, 105 | fns: []string{ 106 | 0: "internal/poll.(*FD).Read", 107 | }, 108 | tags: SpanTagRead, 109 | at: 1, 110 | }, 111 | { 112 | state: StateBlockedNet, 113 | fns: []string{ 114 | 0: "internal/poll.(*FD).Read", 115 | 1: "net.(*netFD).Read", 116 | }, 117 | tags: SpanTagNetwork, 118 | at: 2, 119 | }, 120 | { 121 | state: StateBlockedNet, 122 | fns: []string{ 123 | 0: "internal/poll.(*FD).Accept", 124 | }, 125 | tags: SpanTagAccept, 126 | at: 1, 127 | }, 128 | { 129 | state: StateBlockedNet, 130 | fns: []string{ 131 | 0: "internal/poll.(*FD).Accept", 132 | 1: "net.(*netFD).accept", 133 | }, 134 | at: 2, 135 | tags: SpanTagNetwork, 136 | }, 137 | { 138 | state: StateBlockedNet, 139 | fns: []string{ 140 | 0: "internal/poll.(*FD).Accept", 141 | 2: "net.(*TCPListener).accept", 142 | 3: "net.(*TCPListener).Accept", 143 | }, 144 | at: 4, 145 | tags: SpanTagTCP, 146 | }, 147 | { 148 | state: StateBlockedNet, 149 | relFns: [][]string{ 150 | {"net.(*sysDialer).dialSingle"}, 151 | }, 152 | tags: SpanTagDial, 153 | }, 154 | { 155 | state: StateBlockedNet, 156 | relFns: [][]string{ 157 | {"net.(*sysDialer).dialTCP"}, 158 | }, 159 | tags: SpanTagTCP, 160 | }, 161 | 162 | { 163 | state: StateBlockedNet, 164 | relFns: [][]string{ 165 | {"crypto/tls.(*Conn).readFromUntil"}, 166 | }, 167 | tags: SpanTagTLS, 168 | }, 169 | { 170 | state: StateBlockedNet, 171 | relFns: [][]string{ 172 | {"crypto/tls.(*listener).Accept"}, 173 | }, 174 | tags: SpanTagTLS, 175 | }, 176 | 177 | { 178 | state: StateBlockedNet, 179 | relFns: [][]string{ 180 | {"net/http.(*connReader).Read"}, 181 | }, 182 | tags: SpanTagHTTP, 183 | }, 184 | { 185 | state: StateBlockedNet, 186 | relFns: [][]string{ 187 | {"net/http.(*persistConn).Read"}, 188 | }, 189 | tags: SpanTagHTTP, 190 | }, 191 | { 192 | state: StateBlockedNet, 193 | relFns: [][]string{ 194 | {"net/http.(*http2clientConnReadLoop).run"}, 195 | }, 196 | tags: SpanTagHTTP, 197 | }, 198 | { 199 | state: StateBlockedNet, 200 | relFns: [][]string{ 201 | {"net/http.(*Server).Serve"}, 202 | }, 203 | tags: SpanTagHTTP, 204 | }, 205 | }, 206 | 207 | StateBlockedSelect: { 208 | { 209 | state: StateBlockedSelect, 210 | at: 1, 211 | }, 212 | }, 213 | } 214 | 215 | func applyPatterns(tr *Trace, s Span, pcs []uint64) Span { 216 | // OPT(dh): be better than O(n) 217 | 218 | patternLoop: 219 | for _, p := range patterns[s.State] { 220 | if len(pcs) < len(p.fns) { 221 | continue 222 | } 223 | 224 | for i, fn := range p.fns { 225 | if fn == "" { 226 | continue 227 | } 228 | if tr.PCs[pcs[i]].Func != fn { 229 | continue patternLoop 230 | } 231 | } 232 | 233 | for _, relFns := range p.relFns { 234 | matched := false 235 | 236 | // OPT(dh): be better than O(n²) 237 | offsetLoop: 238 | for start := 0; start < len(pcs); start++ { 239 | if len(pcs)-start < len(relFns) { 240 | break 241 | } 242 | 243 | for i, fn := range relFns { 244 | if fn == "" { 245 | continue 246 | } 247 | if tr.PCs[pcs[i+start]].Func != fn { 248 | continue offsetLoop 249 | } 250 | } 251 | 252 | matched = true 253 | break 254 | } 255 | 256 | if !matched { 257 | continue patternLoop 258 | } 259 | } 260 | 261 | if p.at != 0 && int(p.at) >= len(pcs) { 262 | continue 263 | } 264 | 265 | if p.at != 0 { 266 | if int(p.at) < len(pcs) { 267 | s.At = p.at 268 | } else { 269 | continue 270 | } 271 | } 272 | 273 | if p.newState != StateNone { 274 | s.State = p.newState 275 | } 276 | 277 | s.Tags |= p.tags 278 | } 279 | 280 | return s 281 | } 282 | -------------------------------------------------------------------------------- /trace/ptrace/statistics.go: -------------------------------------------------------------------------------- 1 | package ptrace 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sort" 7 | "time" 8 | ) 9 | 10 | func ComputeProcessorBusy(tr *Trace, p *Processor, bucketSize time.Duration) []int { 11 | total := tr.Duration() 12 | buckets := make([]time.Duration, int(math.Ceil(float64(total)/float64(bucketSize)))) 13 | for i := 0; i < len(p.Spans); i++ { 14 | span := p.Spans[i] 15 | d := time.Duration(span.End - span.Start) 16 | bucket := time.Duration(span.Start-tr.Start()) / bucketSize 17 | bucketRemainder := bucketSize - time.Duration(span.Start-tr.Start())%bucketSize 18 | 19 | for d > bucketRemainder { 20 | buckets[bucket] += bucketRemainder 21 | d -= bucketRemainder 22 | bucket++ 23 | bucketRemainder = bucketSize 24 | } 25 | if d > 0 { 26 | buckets[bucket] += d 27 | } 28 | } 29 | 30 | out := make([]int, len(buckets)) 31 | for i, n := range buckets { 32 | if n > bucketSize { 33 | panic(fmt.Sprintf("bucket %d has value %d, which exceeds bucket size of %d", i, n, bucketSize)) 34 | } 35 | out[i] = int(math.Round(float64(n) / float64(bucketSize) * 100)) 36 | } 37 | 38 | return out 39 | } 40 | 41 | // Spans is an interface that allows ComputeStatistics to operate on abstract collections of spans, not just slices. 42 | type Spans interface { 43 | AtPtr(idx int) *Span 44 | Len() int 45 | } 46 | 47 | func ToSpans(spans []Span) Spans { 48 | return spansSlice(spans) 49 | } 50 | 51 | type spansSlice []Span 52 | 53 | func (spans spansSlice) AtPtr(idx int) *Span { return &spans[idx] } 54 | func (spans spansSlice) Len() int { return len(spans) } 55 | 56 | func ComputeStatistics(spans Spans) Statistics { 57 | var values [StateLast][]time.Duration 58 | 59 | var stats Statistics 60 | 61 | for i := range values { 62 | values[i] = values[i][:0] 63 | } 64 | 65 | for i := 0; i < spans.Len(); i++ { 66 | s := spans.AtPtr(i) 67 | stat := &stats[s.State] 68 | stat.Count++ 69 | d := s.Duration() 70 | if d > stat.Max { 71 | stat.Max = d 72 | } 73 | if d < stat.Min || stat.Min == 0 { 74 | stat.Min = d 75 | } 76 | stat.Total += d 77 | values[s.State] = append(values[s.State], d) 78 | } 79 | 80 | for state := range stats { 81 | stat := &stats[state] 82 | 83 | if len(values[state]) == 0 { 84 | continue 85 | } 86 | 87 | stat.Average = float64(stat.Total) / float64(len(values[state])) 88 | 89 | sort.Slice(values[state], func(i, j int) bool { 90 | return values[state][i] < values[state][j] 91 | }) 92 | 93 | if len(values[state])%2 == 0 { 94 | mid := len(values[state]) / 2 95 | stat.Median = float64(values[state][mid]+values[state][mid-1]) / 2 96 | } else { 97 | stat.Median = float64(values[state][len(values[state])/2]) 98 | } 99 | } 100 | 101 | return stats 102 | } 103 | -------------------------------------------------------------------------------- /trace/ptrace/validate.go: -------------------------------------------------------------------------------- 1 | package ptrace 2 | 3 | var legalStateTransitions = [256][StateLast]bool{ 4 | StateInactive: { 5 | StateActive: true, 6 | StateReady: true, 7 | StateBlockedSyscall: true, 8 | 9 | // Starting back into preempted mark assist 10 | StateGCMarkAssist: true, 11 | }, 12 | StateActive: { 13 | // active -> ready occurs on preemption 14 | StateReady: true, 15 | StateInactive: true, 16 | StateBlocked: true, 17 | StateBlockedSend: true, 18 | StateBlockedRecv: true, 19 | StateBlockedSelect: true, 20 | StateBlockedSync: true, 21 | StateBlockedSyncOnce: true, 22 | StateBlockedSyncTriggeringGC: true, 23 | StateBlockedCond: true, 24 | StateBlockedNet: true, 25 | StateBlockedGC: true, 26 | StateBlockedSyscall: true, 27 | StateStuck: true, 28 | StateDone: true, 29 | StateGCMarkAssist: true, 30 | StateGCSweep: true, 31 | }, 32 | StateGCIdle: { 33 | // active -> ready occurs on preemption 34 | StateReady: true, 35 | StateInactive: true, 36 | StateBlockedSync: true, 37 | }, 38 | StateGCDedicated: { 39 | // active -> ready occurs on preemption 40 | StateReady: true, 41 | StateInactive: true, 42 | StateBlockedSync: true, 43 | }, 44 | StateGCFractional: { 45 | // active -> ready occurs on preemption 46 | StateReady: true, 47 | StateInactive: true, 48 | StateBlockedSync: true, 49 | }, 50 | StateCreated: { 51 | StateActive: true, 52 | 53 | // FIXME(dh): These three transitions are only valid for goroutines that already existed when tracing started. 54 | // eventually we'll make it so those goroutines don't end up in stateReady, at which point we should remove 55 | // these entries. 56 | StateInactive: true, 57 | StateBlocked: true, 58 | StateBlockedSyscall: true, 59 | }, 60 | StateReady: { 61 | StateActive: true, 62 | StateGCMarkAssist: true, 63 | StateGCIdle: true, 64 | StateGCDedicated: true, 65 | StateGCFractional: true, 66 | }, 67 | StateBlocked: {StateReady: true}, 68 | StateBlockedSend: {StateReady: true}, 69 | StateBlockedRecv: {StateReady: true}, 70 | StateBlockedSelect: {StateReady: true}, 71 | StateBlockedSync: {StateReady: true}, 72 | StateBlockedSyncOnce: {StateReady: true}, 73 | StateBlockedSyncTriggeringGC: {StateReady: true}, 74 | StateBlockedCond: {StateReady: true}, 75 | StateBlockedNet: {StateReady: true}, 76 | StateBlockedGC: {StateReady: true}, 77 | StateBlockedSyscall: { 78 | StateReady: true, 79 | }, 80 | 81 | StateGCMarkAssist: { 82 | // active -> ready occurs on preemption 83 | StateReady: true, 84 | StateActive: true, // back to the goroutine's previous state 85 | StateInactive: true, // mark assist can be preempted 86 | StateBlocked: true, 87 | StateBlockedSync: true, 88 | StateBlockedGC: true, // XXX what does this transition mean? 89 | }, 90 | 91 | StateGCSweep: { 92 | StateActive: true, // back to the goroutine's previous state 93 | }, 94 | } 95 | -------------------------------------------------------------------------------- /unsafe/unsafe.go: -------------------------------------------------------------------------------- 1 | package unsafe 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "golang.org/x/exp/constraints" 7 | ) 8 | 9 | func Cast[Dst, Src any](x Src) Dst { 10 | return *(*Dst)(unsafe.Pointer(&x)) 11 | } 12 | 13 | func SliceCast[Dst ~[]DstE, Src ~[]SrcE, DstE, SrcE any](x Src) Dst { 14 | // We don't use our Cast helper in this function because it increases the function complexity, making inlining 15 | // more difficult. 16 | 17 | type sliceHeader struct { 18 | data unsafe.Pointer 19 | len int 20 | cap int 21 | } 22 | 23 | if cap(x) == 0 { 24 | return nil 25 | } 26 | 27 | // This way of getting the pointer has lower inlining complexity than &x[:1][0] 28 | ptrDst := (*sliceHeader)(unsafe.Pointer(&x)).data 29 | 30 | sizeSrc := unsafe.Sizeof(*new(SrcE)) 31 | sizeDst := unsafe.Sizeof(*new(DstE)) 32 | 33 | if sizeSrc >= sizeDst { 34 | return *(*Dst)(unsafe.Pointer(&sliceHeader{ 35 | data: ptrDst, 36 | len: len(x) * int(sizeSrc/sizeDst), 37 | cap: cap(x) * int(sizeSrc/sizeDst), 38 | })) 39 | } else { 40 | return *(*Dst)(unsafe.Pointer(&sliceHeader{ 41 | data: ptrDst, 42 | len: len(x) / int(sizeDst/sizeSrc), 43 | cap: cap(x) / int(sizeDst/sizeSrc), 44 | })) 45 | } 46 | } 47 | 48 | func Index[E any, S ~[]E, Int constraints.Integer](ptr S, idx Int) *E { 49 | offset := unsafe.Sizeof(*new(E)) * uintptr(idx) 50 | return (*E)(unsafe.Add(unsafe.Pointer(&ptr[0]), offset)) 51 | } 52 | -------------------------------------------------------------------------------- /widget/alias.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "gioui.org/widget" 4 | 5 | type Image = widget.Image 6 | type Scrollbar = widget.Scrollbar 7 | type Editor = widget.Editor 8 | type ChangeEvent = widget.ChangeEvent 9 | type SubmitEvent = widget.SubmitEvent 10 | type Label = widget.Label 11 | type Selectable = widget.Selectable 12 | -------------------------------------------------------------------------------- /widget/bool.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "context" 5 | rtrace "runtime/trace" 6 | 7 | "honnef.co/go/gotraceui/layout" 8 | 9 | "gioui.org/io/semantic" 10 | ) 11 | 12 | type Bool struct { 13 | Value bool 14 | 15 | clk PrimaryActivatable 16 | } 17 | 18 | func (b *Bool) Get() bool { return b.Value } 19 | func (b *Bool) Set(v bool) { b.Value = v } 20 | 21 | // Update the widget state and report whether Value was changed. 22 | func (b *Bool) Update(gtx layout.Context) bool { 23 | changed := false 24 | for b.clk.Clicked(gtx) { 25 | b.Value = !b.Value 26 | changed = true 27 | } 28 | return changed 29 | } 30 | 31 | // Hovered reports whether pointer is over the element. 32 | func (b *Bool) Hovered() bool { 33 | return b.clk.Hovered() 34 | } 35 | 36 | // Pressed reports whether pointer is pressing the element. 37 | func (b *Bool) Pressed() bool { 38 | return b.clk.Pressed() 39 | } 40 | 41 | // Focused reports whether b has focus. 42 | func (b *Bool) Focused() bool { 43 | return b.clk.Focused() 44 | } 45 | 46 | func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { 47 | defer rtrace.StartRegion(context.Background(), "widget.Bool.Layout").End() 48 | 49 | b.Update(gtx) 50 | dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 51 | semantic.SelectedOp(b.Value).Add(gtx.Ops) 52 | semantic.EnabledOp(gtx.Queue != nil).Add(gtx.Ops) 53 | return w(gtx) 54 | }) 55 | return dims 56 | } 57 | 58 | type BackedBit[T uint8 | uint16 | uint32 | uint64] struct { 59 | Bits *T 60 | Bit int 61 | 62 | clk PrimaryActivatable 63 | 64 | changed bool 65 | } 66 | 67 | func (bit *BackedBit[T]) Get() bool { 68 | return *bit.Bits&(1<= 0; i-- { 63 | if mod >= g.keys[i] { 64 | frame = i 65 | break 66 | } 67 | } 68 | if frame == -1 { 69 | panic("unreachable") 70 | } 71 | 72 | // OPT(dh): set invalidation in time for next frame 73 | op.InvalidateOp{}.Add(gtx.Ops) 74 | 75 | // OPT(dh): cache image ops 76 | // OPT(dh): Gio deletes a texture from the GPU if it hasn't been used in a single frame. That is, of course, 77 | // unfortunate for an animation. 78 | // TODO(dh): allow setting Image.Fit 79 | return widget.Image{Src: g.imgs[frame], Position: dir, Scale: 1.0 / gtx.Metric.PxPerDp}.Layout(gtx) 80 | } 81 | -------------------------------------------------------------------------------- /widget/histogram.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | "time" 7 | ) 8 | 9 | const DefaultHistogramBins = 100 10 | 11 | type FloatDuration float64 12 | 13 | func (d FloatDuration) Floor() time.Duration { 14 | return time.Duration(math.Floor(float64(d))) 15 | } 16 | 17 | func (d FloatDuration) Ceil() time.Duration { 18 | return time.Duration(math.Ceil(float64(d))) 19 | } 20 | 21 | type Histogram struct { 22 | // The config that was passed to NewHistogram 23 | Config *HistogramConfig 24 | Start FloatDuration 25 | Bins []int 26 | BinWidth FloatDuration 27 | Overflow FloatDuration 28 | MaxValue time.Duration 29 | MaxBinValue int 30 | } 31 | 32 | func quartiles(data []time.Duration) (first, second, third float64) { 33 | if len(data) == 0 { 34 | return 0, 0, 0 35 | } 36 | if len(data) == 1 { 37 | v := float64(data[0]) 38 | return v, v, v 39 | } 40 | 41 | median := func(points []time.Duration) float64 { 42 | if len(points)%2 == 1 { 43 | return float64(points[len(points)/2]) 44 | } else { 45 | return (float64(points[len(points)/2]) + float64(points[len(points)/2-1])) / 2 46 | } 47 | } 48 | 49 | var left, right []time.Duration 50 | if len(data)%2 == 1 { 51 | left = data[0 : len(data)/2] 52 | right = data[len(data)/2+1:] 53 | } else { 54 | left = data[0 : len(data)/2] 55 | right = data[len(data)/2:] 56 | } 57 | 58 | if len(left) == 0 { 59 | first = second 60 | } else { 61 | first = median(left) 62 | } 63 | second = median(data) 64 | if len(right) == 0 { 65 | third = second 66 | } else { 67 | third = median(right) 68 | } 69 | 70 | return 71 | } 72 | 73 | type HistogramConfig struct { 74 | Start, End FloatDuration 75 | RejectOutliers bool 76 | Bins int 77 | } 78 | 79 | func NewHistogram(cfg *HistogramConfig, values []time.Duration) *Histogram { 80 | var ( 81 | start, end FloatDuration 82 | rejectOutliers bool 83 | bins int 84 | ) 85 | 86 | if cfg != nil { 87 | if cfg.Bins == 0 { 88 | cfg.Bins = DefaultHistogramBins 89 | } 90 | start, end = cfg.Start, cfg.End 91 | rejectOutliers = cfg.RejectOutliers 92 | bins = cfg.Bins 93 | } 94 | 95 | if bins == 0 { 96 | bins = DefaultHistogramBins 97 | } 98 | 99 | if end != 0 { 100 | rejectOutliers = false 101 | } 102 | 103 | if rejectOutliers { 104 | sort.Slice(values, func(i, j int) bool { return values[i] < values[j] }) 105 | 106 | first, _, third := quartiles(values) 107 | iqr := third - first 108 | cutoff := third + 2.5*iqr 109 | 110 | firstCutoffIdx := sort.Search(len(values), func(i int) bool { 111 | return float64(values[i]) >= cutoff 112 | }) 113 | 114 | if firstCutoffIdx == 0 { 115 | // If all values are outliers then none are. This should only happen when there is exactly 1 value. 116 | firstCutoffIdx = len(values) 117 | } 118 | 119 | var binWidth FloatDuration 120 | if firstCutoffIdx > 0 { 121 | lastFittingValue := FloatDuration(values[firstCutoffIdx-1]) 122 | if end != 0 { 123 | lastFittingValue = end 124 | } 125 | binWidth = FloatDuration(lastFittingValue-start) / FloatDuration(bins) 126 | if binWidth == 0 { 127 | binWidth = 1 128 | } 129 | } else { 130 | bins = 0 131 | } 132 | 133 | var overflow FloatDuration 134 | if firstCutoffIdx < len(values) { 135 | bins++ 136 | overflow = start + binWidth*FloatDuration(bins) 137 | } 138 | 139 | hist := &Histogram{ 140 | Config: cfg, 141 | Start: start, 142 | Overflow: overflow, 143 | BinWidth: binWidth, 144 | Bins: make([]int, bins), 145 | } 146 | 147 | // We've sorted the values to find the median. This means we don't have to compute the bin index for each value, 148 | // only when the value falls out of the previous bin. The extra branch is much cheaper than the division. 149 | var curBin int 150 | var curEnd FloatDuration = 1 * binWidth 151 | for _, v := range values[:firstCutoffIdx] { 152 | v := FloatDuration(v) 153 | if v >= curEnd { 154 | // Truncate, don't round, to find the bucket 155 | curBin = int((v - start) / binWidth) 156 | curEnd = FloatDuration(curBin+1) * binWidth 157 | if curBin == len(hist.Bins) { 158 | // If the maximum value is 10 and we have 10 bins, then the final bin has to be [9, 10] instead of [9, 10). 159 | curBin-- 160 | } 161 | } 162 | 163 | hist.Bins[curBin]++ 164 | } 165 | if hist.Overflow > 0 || binWidth == 0 { 166 | hist.Bins[bins-1] += len(values[firstCutoffIdx:]) 167 | } 168 | 169 | var maxBinValue int 170 | for _, v := range hist.Bins { 171 | if v > maxBinValue { 172 | maxBinValue = v 173 | } 174 | } 175 | 176 | hist.MaxValue = values[len(values)-1] 177 | hist.MaxBinValue = maxBinValue 178 | 179 | return hist 180 | } else { 181 | var maxValue time.Duration 182 | for _, v := range values { 183 | if v > maxValue { 184 | maxValue = v 185 | } 186 | } 187 | 188 | var binWidth FloatDuration 189 | lastFittingValue := FloatDuration(maxValue) 190 | if end != 0 { 191 | lastFittingValue = end 192 | } 193 | binWidth = FloatDuration(lastFittingValue-start) / FloatDuration(bins) 194 | if binWidth == 0 { 195 | binWidth = 1 196 | } 197 | 198 | hist := &Histogram{ 199 | Config: cfg, 200 | Start: start, 201 | BinWidth: binWidth, 202 | Bins: make([]int, bins), 203 | } 204 | 205 | for _, v := range values { 206 | v := FloatDuration(v) 207 | if v < start || v > lastFittingValue { 208 | // The user provided values that are out of range for start and end. Reject these values. 209 | continue 210 | } 211 | // Truncate, don't round, to find the bucket 212 | curBin := int((v - start) / binWidth) 213 | if curBin == len(hist.Bins) { 214 | // If the maximum value is 10 and we have 10 bins, then the final bin has to be [9, 10] instead of [9, 10). 215 | curBin-- 216 | } 217 | 218 | hist.Bins[curBin]++ 219 | } 220 | 221 | var maxBinValue int 222 | for _, v := range hist.Bins { 223 | if v > maxBinValue { 224 | maxBinValue = v 225 | } 226 | } 227 | 228 | hist.MaxValue = maxValue 229 | hist.MaxBinValue = maxBinValue 230 | 231 | return hist 232 | } 233 | } 234 | 235 | func (hist *Histogram) HasOverflow() bool { 236 | return hist.Overflow > 0 || hist.BinWidth == 0 237 | } 238 | 239 | func (hist *Histogram) BucketRange(i int) (start, end FloatDuration) { 240 | start = hist.Start + hist.BinWidth*FloatDuration(i) 241 | if !hist.HasOverflow() || i < len(hist.Bins)-1 { 242 | end = hist.Start + hist.BinWidth*FloatDuration(i+1) 243 | } else { 244 | end = FloatDuration(hist.MaxValue) + 1 245 | } 246 | 247 | return start, end 248 | } 249 | -------------------------------------------------------------------------------- /widget/widget.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "image/color" 5 | 6 | "honnef.co/go/gotraceui/layout" 7 | 8 | "gioui.org/op" 9 | "gioui.org/op/paint" 10 | ) 11 | 12 | func ColorTextMaterial(gtx layout.Context, c color.NRGBA) op.CallOp { 13 | m := op.Record(gtx.Ops) 14 | paint.ColorOp{Color: c}.Add(gtx.Ops) 15 | return m.Stop() 16 | } 17 | 18 | type List struct { 19 | Main Scrollbar 20 | Cross Scrollbar 21 | CrossOffset float32 22 | Widest int 23 | layout.List 24 | } 25 | --------------------------------------------------------------------------------