Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
58 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

A very long step log stays fast. Each chunk of a job's log is read once, into its step's log, as it arrives (`appendLog` in lib/log-lines.ts), where before every chunk joined the step's text again and read all of it from the start: a group a chunk leaves open holds the next chunk's lines, a line still being written shows until it ends, and a search reads the lines already read, once a change. A step of more than 2,000 rows draws only those on screen and 800 pixels either side (lib/log-window.ts), each row one line high until it is drawn and measured, so the space kept above and below is right however lines wrap; groups fold in the page's state rather than in a `details`, and search, following the newest line and Jump to latest work as on a short log. Each line's number links to it as GitHub's do (`#step:3:12`), opening its step and its group, scrolling to it and marking it. With a synthetic 50,000-line log in 500 chunks of 100 lines, reading it took 24.6 s in all and 105 ms for the last chunk before, and 0.5 s and 1.3 ms now; in Chromium the page drew the whole log in 2.2 s and each new chunk in 213 ms (157,000 elements), and now in 51 ms and 17 ms (385 elements). The Actions guide says so.1/**
2 * Drawing a long log a screenful at a time. A step's log past a few
3 * thousand rows draws only the rows on screen and a few around them, with
4 * space above and below standing in for the rest, so a 50,000-line log
5 * opens, grows and scrolls as fast as a short one. Rows are one line high
6 * until they are drawn and measured (a long line wraps), and the space
7 * kept for them is corrected then.
8 */
9
10/** Past this many rows, a step's log draws only what is on screen. */
11export const WINDOW_FROM = 2000;
12/** A row's height before it is measured: one line of `text-xs leading-5`. */
13export const ROW_HEIGHT = 20;
14/** How far past the screen rows are drawn, above and below, in pixels. */
15export const OVERSCAN = 800;
16
17/**
18 * Where each row starts, from the top of the log: `starts[i]` for row `i`,
19 * and `starts[rows]` for the log's whole height. `height(i)` is a row's
20 * measured height, or undefined for the estimate.
21 */
22export function rowStarts(rows: number, height: (index: number) => number | undefined): Float64Array {
23 const starts = new Float64Array(rows + 1);
24 for (let i = 0; i < rows; i++) starts[i + 1] = starts[i]! + (height(i) ?? ROW_HEIGHT);
25 return starts;
26}
27
28/** The first row that ends below `y`: the row at `y`, or the last. */
29export function rowAt(starts: Float64Array, y: number): number {
30 const rows = starts.length - 1;
31 let low = 0;
32 let high = rows;
33 while (low < high) {
34 const middle = (low + high) >> 1;
35 if (starts[middle + 1]! <= y) low = middle + 1;
36 else high = middle;
37 }
38 return Math.min(low, Math.max(0, rows - 1));
39}
40
41/**
42 * The rows to draw, `[from, to)`, for a view from `top` to `bottom`
43 * (pixels from the top of the log; either may be past its ends), with
44 * `overscan` pixels more either side.
45 */
46export function windowOf(starts: Float64Array, top: number, bottom: number, overscan = OVERSCAN): [number, number] {
47 const rows = starts.length - 1;
48 if (rows === 0) return [0, 0];
49 const total = starts[rows]!;
50 const from = top - overscan;
51 const to = bottom + overscan;
52 if (to <= 0 || from >= total) {
53 // Off screen: draw a few rows at the end nearest the view, so the
54 // log has something to measure and its end can be followed.
55 return from >= total ? [Math.max(0, rows - 20), rows] : [0, Math.min(rows, 20)];
56 }
57 return [rowAt(starts, Math.max(0, from)), rowAt(starts, to) + 1];
58}