render

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

Documentation

Overview

Package render drives a headless Chromium session: it loads a page, waits for it to stop moving, sweeps the viewport through the full scroll height, and hands back a deduplicated capture of everything that was ever on screen.

Index

Constants

This section is empty.

Variables

View Source
var ErrChromiumNotFound = errors.New(
	"no Chromium or Chrome binary found\n" +
		"  sieve needs a Chromium-based browser to render pages.\n" +
		"  Install Google Chrome, Chromium or Microsoft Edge, or point sieve at one:\n" +
		"    sieve distill <url> --chrome /path/to/chrome\n" +
		"    SIEVE_CHROME=/path/to/chrome sieve distill <url>")

ErrChromiumNotFound is returned when no browser binary could be located. It carries instructions rather than a stack trace, because a missing Chromium is the single most common first-run failure.

View Source
var Version = "0.2.0"

Version identifies the build. It is stamped into every trace so an artifact can be tied back to the code that produced it.

A var rather than a const so a release build can stamp a commit onto it.

Functions

func ChromiumPath

func ChromiumPath(explicit string) string

ChromiumPath reports the browser sieve would use, for `sieve doctor`.

func DefaultBlockHosts

func DefaultBlockHosts() []string

DefaultBlockHosts is the analytics, advertising and session-replay traffic that contributes nothing to a page's content and a great deal to its load time. Blocking happens at name resolution, so the cost is zero per request rather than one interception round trip per request.

Session replay tools in particular (FullStory, Hotjar, Clarity) both slow the page and record the sweep, so declining to talk to them is the courteous default as well as the fast one.

func KillBrowsers

func KillBrowsers() int

KillBrowsers terminates every browser this process launched, and their children, without waiting for anything to agree.

Called from the watchdog immediately before the process exits. It is deliberately violent: whatever state the browser is in, we have already concluded it is not answering, and asking politely is the bet that just lost. It reports how many it killed so the watchdog can say so.

func LibraryNotes

func LibraryNotes(found []string) []string

LibraryNotes returns the maintenance notes for the detected libraries.

func LibraryWeight

func LibraryWeight(found []string) (float64, string)

LibraryWeight returns the highest weight among the detected libraries, which is what the escalation scorer consumes. The maximum rather than the sum: one scroll hijacker is already decisive, and five reveal libraries are not five times as decisive.

Types

type Asset

type Asset struct {
	URL  string
	MIME string
	Body []byte
}

Asset is an intercepted response body.

type Browser

type Browser struct {
	// contains filtered or unexported fields
}

Browser owns one Chromium process. Reuse it across pages: process startup is 200-600ms, which dominates the cost of a small page.

func Launch

func Launch(ctx context.Context, opts Options) (*Browser, error)

Launch starts a browser process configured for extraction rather than for browsing: no GPU rasterisation surprises, no first-run dialogs, no background networking, and name resolution for tracker hosts pointed at nothing.

func (*Browser) ChromiumVersion

func (b *Browser) ChromiumVersion() string

ChromiumVersion reports the browser build, for diagnostics and traces.

func (*Browser) Close

func (b *Browser) Close()

Close shuts the browser down. It is safe to call more than once.

Cancelling the allocator asks chromedp to stop the browser and then waits for the process to actually exit. That wait has no bound, and a Chromium that has stopped responding never provides one: one corpus run reached this line after a perfectly good extraction and did not return to its caller for another fourteen minutes, with the artifact already written to disk.

So the polite shutdown gets a deadline, and past it the process tree is killed outright. Nothing is lost by doing so -- everything this browser was for has already been read out of it -- and the alternative is a command that never ends.

func (*Browser) ProbeFrameProduction

func (b *Browser) ProbeFrameProduction(ctx context.Context) (FrameProbe, error)

ProbeFrameProduction verifies the least obvious precondition in the whole renderer.

A headless tab that is not being composited never runs the rendering steps. requestAnimationFrame stops firing, and so does IntersectionObserver, which is what nearly every scroll-reveal animation uses to decide when to show content. Under that condition a sweep completes quickly, reports no errors, and produces an artifact containing the hero and nothing else -- the exact failure this project exists to prevent, arriving silently.

The probe runs on a *secondary* tab on purpose. That is where the failure occurs: the first tab a browser opens composites fine, so a check that used it would pass on a machine where every real sweep is starved.

func (*Browser) SetOptions

func (b *Browser) SetOptions(o Options)

SetOptions adjusts per-sweep settings on a running browser.

The flags a Chromium process was launched with cannot change, so only the sweep-level knobs are honoured -- checkpoint counts, budgets, gates. This is what lets one warm browser serve both a single post-settle capture and a full sweep without paying process startup twice.

func (*Browser) Sweep

func (b *Browser) Sweep(ctx context.Context, rawURL string, guard NavGuard) (*Result, error)

Sweep renders one page and returns the deduplicated capture.

The shape of the work is: navigate, wait for the page to stop moving, then walk the viewport down the document taking a full extraction at each stop, folding each one into a running deduplicated set. It ends when the set stops growing, when the document ends, or when a budget runs out, whichever comes first.

type CanvasShot

type CanvasShot struct {
	Path       string
	PNG        []byte
	Share      float64
	Checkpoint int
	// Uniform is set when the crop is a single flat colour, which means there
	// is nothing in it for a vision model to describe and the expensive path
	// should be skipped.
	Uniform bool
}

CanvasShot is a rasterised canvas region awaiting recovery.

type FrameProbe

type FrameProbe struct {
	// Ticks is how many animation frames fired within the probe window.
	Ticks int `json:"ticks"`
	// IO reports whether an IntersectionObserver callback was delivered.
	IO bool `json:"intersection_observer"`
	// Elapsed is how long the probe took.
	Elapsed time.Duration `json:"elapsed"`
}

FrameProbe reports whether the browser is actually producing frames.

type LibrarySpec

type LibrarySpec struct {
	// Name is the identifier reported in the artifact.
	Name string `json:"n"`
	// Global is a dotted path on `window` whose existence proves the library is
	// loaded, e.g. "gsap.ScrollTrigger".
	Global string `json:"g,omitempty"`
	// Selector is a CSS selector that proves it, for libraries that leave a
	// marker in the DOM rather than a global.
	Selector string `json:"s,omitempty"`
	// Weight is how strongly this library predicts that a cheap fetch will miss
	// content, from 0 to 1. A scroll hijacker is decisive; a tooltip library
	// says nothing.
	Weight float64 `json:"w"`
	// Class groups detectors for reporting: scroll, reveal, 3d, text, router.
	Class string `json:"c"`
	// Note explains what breaks, and is surfaced in `sieve doctor`.
	Note string `json:"note,omitempty"`
}

LibrarySpec is one detector for an animation, scroll or 3D library.

These live in a data file rather than in code on purpose. The tail of animation-library behaviours is endless and each new one is discovered by a site breaking; a contributor should be able to add a detector with a fixture and a pull request against JSON, without touching Go and without waiting for a release.

func LibrarySpecs

func LibrarySpecs() ([]LibrarySpec, error)

LibrarySpecs returns the versioned detector set.

type NavGuard func(u *url.URL) error

NavGuard vets a URL the browser is about to navigate to. It is called for the initial navigation and again for every redirect, because a URL that passed once says nothing about where it points after three hops.

Returning a non-nil error aborts that navigation. The safety package supplies the real implementation; render only defines the shape so the two packages stay independent.

type Options

type Options struct {
	// ChromePath is an explicit browser binary. Empty means auto-detect.
	ChromePath string
	// Headless runs without a visible window. Turning it off is a debugging aid.
	Headless bool
	// NoSandbox is required inside most containers. It weakens the browser's
	// own isolation, so it is off unless asked for.
	NoSandbox bool

	ViewportW   int
	ViewportH   int
	DeviceScale float64
	UserAgent   string
	// AcceptLanguage is sent on every request and also drives the page's
	// navigator.languages, which some sites use to pick content.
	AcceptLanguage string

	// NavTimeout bounds the initial navigation. Exceeding it is not fatal: what
	// matters is whether a document arrived, not whether every straggling image
	// finished.
	NavTimeout time.Duration
	// FirstSettle bounds the wait for entrance animations to finish before the
	// first capture. It is larger than SettleTimeout because a page gets one
	// chance to finish loading and settling, and capturing a hero mid-fade
	// records it at whatever opacity it happened to have.
	FirstSettle time.Duration
	// SettleTimeout bounds the wait for animation settle after a scroll step. A
	// page with a permanently looping animation never settles, so this is the
	// value that actually ends the wait on such sites -- which is why it is
	// small. The sweep takes many cheap looks rather than a few expensive ones.
	SettleTimeout time.Duration
	// SettleFloor is the shortest that wait may be compressed to when the sweep
	// is rationing its remaining time across the rest of the document.
	SettleFloor time.Duration
	// RevealFloor is the settle wait used when text is being captured and none
	// of it has ever been legible. On such a page the sweep is outrunning the
	// animation, and the remedy is fewer, slower stops rather than more.
	RevealFloor time.Duration
	// SettleFrames is how many consecutive animation frames must show no
	// layout change before the page counts as settled.
	SettleFrames int
	// LoadBudget bounds how long sieve will wait for a page to arrive and stop
	// moving, before it starts reading.
	//
	// It is deliberately separate from Budget, and it is not deducted from it.
	// Charging a site's own loading time to the extraction meant that a page
	// with an intro film or a preloader -- exactly the class of page this tool
	// exists for -- handed the sweep whatever was left, and what was left was
	// often one capture of a loading screen. A budget for reading a page should
	// start when there is a page to read.
	LoadBudget time.Duration
	// SweepBudget bounds the in-page checkpoint loop. The loop rations itself
	// against this: it plans its step size and its per-checkpoint settle wait
	// so that the document is covered within it.
	SweepBudget time.Duration
	// Budget bounds the whole render for one page, navigation included.
	Budget time.Duration
	// Passes is how many times the document is walked.
	//
	// Two is the useful default and the reason is that almost every scroll
	// reveal on the web fires once and stays fired. The first pass is as much a
	// trigger as a capture; the second sees at full opacity what the first
	// could only catch mid-fade, and costs almost nothing to transmit because
	// everything it re-observes is already known. It is a far better use of a
	// second than dwelling a second longer at every checkpoint of one pass,
	// because dwelling only helps the section being dwelt on.
	Passes int

	// StepRatio is the fraction of a viewport height advanced per checkpoint.
	// Below 1.0 so that content revealed at the boundary of two viewports is
	// seen fully at least once.
	StepRatio float64
	// MaxCheckpoints caps the sweep on pages that grow as you scroll.
	MaxCheckpoints int
	// StableCheckpoints is K: stop once this many consecutive checkpoints have
	// added no new unique nodes.
	StableCheckpoints int
	// MaxScrollPx caps total distance travelled, which is the guard against an
	// infinite-scroll feed.
	MaxScrollPx float64
	// NodeBudget caps nodes captured per checkpoint.
	NodeBudget int
	// LatentBudget caps hidden-content nodes per checkpoint. It is separate
	// from NodeBudget so that a page with an enormous hidden menu tree cannot
	// crowd out the content the reader can actually see.
	LatentBudget int

	// CollectCorpus retains inline JSON, hydration blobs and script string
	// literals as a confirm-only index for canvas recovery. It is never a
	// source of content -- see the corroborate package for why that rule is
	// absolute.
	CollectCorpus bool
	// MaxCorpusBytes bounds that index.
	MaxCorpusBytes int

	// ReducedMotion emulates prefers-reduced-motion. Running a second pass with
	// it on and comparing the two is the cheapest available check on whether
	// the reveal machinery was understood correctly.
	ReducedMotion bool

	// Locale and Timezone are pinned rather than inherited from the host, so
	// that the same URL rendered on two machines produces the same artifact.
	Locale   string
	Timezone string

	// BlockHosts are host patterns resolved to nothing by the browser, which
	// removes analytics and ad traffic before a connection is opened rather
	// than after. Wildcards are allowed: "*.doubleclick.net".
	BlockHosts []string
	// BlockURLPatterns are additional URLPattern-syntax rules applied by the
	// browser's network layer, for path-level blocking that a host rule cannot
	// express.
	BlockURLPatterns []string

	// CaptureCanvas enables viewport screenshots at checkpoints where a canvas
	// covers at least CanvasShareGate of the viewport. Screenshots are cropped
	// to the canvas and kept only for canvas recovery.
	CaptureCanvas bool
	// CanvasShareGate is the fraction of viewport area a canvas must cover
	// before it is worth rasterising.
	CanvasShareGate float64

	// CollectAssets keeps response bodies for scene-graph formats so canvas
	// recovery can read node names out of them without refetching.
	CollectAssets bool
	// MaxAssetBytes and MaxAssetsTotal bound that collection.
	MaxAssetBytes  int64
	MaxAssetsTotal int64

	// Proxy routes browser traffic through an HTTP proxy.
	Proxy string

	// Logf receives progress lines. Nil discards them.
	Logf func(format string, args ...any)
}

Options configures a render session. The zero value is not usable; start from DefaultOptions and adjust.

func DefaultOptions

func DefaultOptions() Options

DefaultOptions returns settings tuned for design-heavy marketing and portfolio sites, which is the class of page this tool exists for.

func (*Options) ScaleTo

func (o *Options) ScaleTo(total time.Duration)

ScaleTo fits the render's time budget inside the wall clock the caller is willing to spend on a whole page, holding back a margin for the fetch, the graph and the emit.

The sub-budgets move together and stay in proportion, because they are not independent: a navigation allowance larger than the sweep budget guarantees a page that loads slowly is never swept, and a first-settle wait that is a large fraction of the total guarantees the same. Scaling one number is also the only way `--timeout` can mean what it says. Before this, `--timeout 10m` left the sweep stopping at its own hardcoded two and a half minutes, and `--timeout 10s` left it cheerfully planning for a hundred and fifty.

type Result

type Result struct {
	RequestedURL string
	FinalURL     string
	Status       int64
	Merged       *capture.Merged

	// OpenedDisclosures names the tabs and accordions sieve pressed open, so a
	// reader can tell content that was on screen from content that had to be
	// revealed.
	OpenedDisclosures []string

	// Assets holds response bodies for scene-graph formats, kept so canvas
	// recovery can read them without a second fetch.
	Assets []Asset
	// CanvasShots maps a canvas node path to a PNG cropped to that canvas at
	// the checkpoint where it filled the most of the viewport.
	CanvasShots map[string]*CanvasShot
	// Corpus is the confirm-only membership index over the text the site
	// shipped. It is never a source of content; see the corroborate package.
	Corpus *corroborate.Index
	// Scene is what walking the live 3D scene graph produced, if anything.
	Scene *capture.SceneIntrospection
	// Libraries are the animation, scroll and 3D libraries detected in the page.
	Libraries []string
	// EnteredGate names an entry screen sieve pressed through, when it did.
	EnteredGate string
	// EntryGate names an interstitial the visitor must dismiss before the site
	// begins -- the "click to enter" screen. It is empty when there is none.
	//
	// It matters because the alternative is silence. hatom.com sits behind one,
	// and the artifact reported nine blocks with no indication that the page had
	// not started: indistinguishable, to a reader, from a site that simply has
	// nothing on it.
	EntryGate string

	// Trace is everything needed to reproduce this render.
	Trace Trace

	Timing Timing
	// ReachedBottom reports that the sweep saw the end of the document rather
	// than running out of budget on the way there.
	ReachedBottom bool
	// Notes records anything that limited the sweep, so the artifact can be
	// honest about its own coverage instead of silently reporting a partial
	// page as complete.
	Notes []string
	// Blocked records that the site refused this client, and how it was
	// detected. Partial and honest beats empty.
	Blocked       bool
	BlockedReason string
}

Result is everything one page yielded.

type Timing

type Timing struct {
	Navigate time.Duration `json:"navigate_ms"`
	// Load is how long the page took to arrive and stop moving. It is reported
	// separately because it is the site's time, not sieve's, and the extraction
	// budget does not start until it has elapsed.
	Load        time.Duration `json:"load_ms"`
	FirstSettle time.Duration `json:"first_settle_ms"`
	Sweep       time.Duration `json:"sweep_ms"`
	Total       time.Duration `json:"total_ms"`
	Checkpoints int           `json:"checkpoints"`
	Passes      int           `json:"passes"`
	SettleWaits int           `json:"settle_waits"`
	SettleMiss  int           `json:"settle_timeouts"`
}

Timing breaks the wall clock down so a slow site can be diagnosed without re-running with a profiler attached.

type Trace

type Trace struct {
	SieveVersion   string   `json:"sieve_version"`
	CaptureHash    string   `json:"capture_script_sha256"`
	Chromium       string   `json:"chromium"`
	UserAgent      string   `json:"user_agent"`
	ViewportW      int      `json:"viewport_w"`
	ViewportH      int      `json:"viewport_h"`
	DeviceScale    float64  `json:"device_scale"`
	Locale         string   `json:"locale"`
	Timezone       string   `json:"timezone"`
	ReducedMotion  bool     `json:"reduced_motion"`
	StepRatio      float64  `json:"step_ratio"`
	SettleFrames   int      `json:"settle_frames"`
	SettleTimeout  string   `json:"settle_timeout"`
	FirstSettle    string   `json:"first_settle"`
	SettleFloor    string   `json:"settle_floor"`
	SweepBudget    string   `json:"sweep_budget"`
	Passes         int      `json:"passes"`
	MaxCheckpoints int      `json:"max_checkpoints"`
	StableCheckpts int      `json:"stable_checkpoints"`
	Flags          []string `json:"flags"`
	BlockHostsHash string   `json:"block_hosts_sha256"`
}

Trace is the complete set of inputs that determined a render's output.

A trace missing any of these is not replayable, and an artifact whose trace is not replayable cannot support a bug report: the maintainer would have to reproduce against the live site, on their own machine, with their own Chromium, which is precisely the situation that makes browser-dependent projects expensive to maintain.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL