@ 261101.7

261029.1 · b95235bbfa19defc68c21353ce4014ec9e9976df

A tree page's buffer guess stays near its size, and a helper's crash shows

The builder grew 512 bytes a line before the parse, so a 1 MiB file of blank lines took 512 MB, a file with no grammar paid it too, and a tree of 16k lines overran the cache it was charged to. The guess now waits for the first node and keeps to 20 bytes a byte of source, and a buffer well past its rows is copied down before the cache counts it.

A helper that dies or times out mid-line now reports its own message or the timeout, not the cut line's field count.

reading declarations

bench_test.go

@@ -105,8 +105,8 @@ func BenchmarkCodePage(b *testing.B) {  // BenchmarkSyntaxTree times a tree page's first view on jquery.js in its // stages: the helper with reading its output, rendering the rows, and-// the whole page as a request returns it, where the two overlap. It skips where the helper or-// the file is missing.+// the whole page as a request returns it, where the two overlap. It+// skips where the helper or the file is missing. func BenchmarkSyntaxTree(b *testing.B) { 	src, err := os.ReadFile("/usr/share/javascript/jquery/jquery.js") 	if err != nil {

syntax.go

@@ -82,9 +82,18 @@ func parseSyntax(ctx context.Context, name string, src []byte, row func(synNode) 		return err 	} 	if err := readNodes(out, row); err != nil {+		// A helper that crashed or ran out of time leaves a cut line, so+		// its own message or the timeout says more than the cut does.+		timedOut := ctx.Err() != nil 		// Cancelling kills the helper, so Wait returns. 		cancel() 		cmd.Wait()+		if msg := strings.TrimSpace(stderr.String()); msg != "" {+			return errors.New(msg)+		}+		if timedOut {+			return context.DeadlineExceeded+		} 		return err 	} 	if err := cmd.Wait(); err != nil {@@ -192,8 +201,14 @@ func syntaxPage(ctx context.Context, name, href string, src []byte) (string, err 	} 	h := rows.done() 	// The rows keep the builder's whole buffer, which the guess in-	// newSynRows may make larger than h.-	syntaxCache.put(key, h, int64(rows.b.Cap()))+	// newSynRows may make larger than h. Well past it, h keeps a copy of+	// its own, so the cache holds no more than it counts.+	size := rows.b.Cap()+	if size > len(h)+len(h)/4 {+		h = strings.Clone(h)+		size = len(h)+	}+	syntaxCache.put(key, h, int64(size)) 	return h, nil } @@ -203,6 +218,7 @@ type synRows struct { 	b     strings.Builder 	lines [][]byte 	href  string+	size  int // bytes of source 	open  int // folds left open 	held  synNode 	any   bool // whether a node is held@@ -210,10 +226,7 @@ type synRows struct {  // newSynRows starts the rows of src's tree, with line links to href. func newSynRows(href string, src []byte) *synRows {-	w := &synRows{lines: bytes.Split(src, []byte("\n")), href: href}-	// A row is about 120 bytes, and a line of a large file gives about-	// four, so the builder seldom grows where it would cost the most.-	w.b.Grow(len(w.lines) * 512)+	w := &synRows{lines: bytes.Split(src, []byte("\n")), href: href, size: len(src)} 	w.b.WriteString("<div class=\"syntax\">\n") 	return w }@@ -221,6 +234,13 @@ func newSynRows(href string, src []byte) *synRows { func (w *synRows) add(n synNode) { 	if w.any { 		w.row(w.held, n.depth > w.held.depth)+	} else {+		// A row is about 120 bytes, and a line of a large file gives about+		// four, so the builder seldom grows where it would cost the most.+		// A file of short or blank lines gives fewer, and its rows come to+		// about 17 bytes a byte of it, so the guess keeps to 20. It waits+		// for the first node, so a file with no tree costs nothing.+		w.b.Grow(min(len(w.lines)*512, w.size*20)) 	} 	w.held, w.any = n, true }

syntax_test.go

@@ -63,13 +63,14 @@ func TestSyntaxBadOutput(t *testing.T) { 		{`printf '0\tsource_file\t\t0\t0\n'`, "a line of 5 fields"}, 		{`printf '0\tsource_file\t\t0\t-1\t3\t0\n'`, "a bad number: -1"}, 		{`printf '0\tsource_file\t\t0\t0\t3\t0\n'; echo 'E5: broke' >&2; exit 5`, "E5: broke"},+		{`printf '0\tsource_file\t\t0'; echo 'E5: broke' >&2; exit 5`, "E5: broke"}, 	} { 		dir := t.TempDir() 		script := "#!/bin/sh\ncat >/dev/null\n" + tc.script + "\n" 		if err := os.WriteFile(filepath.Join(dir, progName+"-syntax"), []byte(script), 0o755); err != nil { 			t.Fatal(err) 		}-		t.Setenv("PATH", dir)+		t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) 		if _, err := syntaxPage(context.Background(), "a.go", "a.go", []byte("x\n")); err == nil || !strings.Contains(err.Error(), tc.want) { 			t.Errorf("%s: %v, want %q", tc.script, err, tc.want) 		}