Skip to content

fix(v3): keep oversized event payloads out of evaluateJavaScript - #5930

Merged
leaanthony merged 10 commits into
masterfrom
fix/oversized-event-payloads
Aug 9, 2026
Merged

fix(v3): keep oversized event payloads out of evaluateJavaScript#5930
leaanthony merged 10 commits into
masterfrom
fix/oversized-event-payloads

Conversation

@leaanthony

@leaanthony leaanthony commented Aug 9, 2026

Copy link
Copy Markdown
Member

Description

Go→JS events are delivered by splicing the event JSON into a JavaScript source string and evaluating it in the webview. Above a size threshold WebKit switches from inline IPC message data to an out-of-line shared-memory transfer, and those regions are retained for as long as the app keeps emitting.

At 100 events/sec of 1 MB payloads:

platform process affected peak
macOS 26.4.1 (WebKit-Cocoa) host (UI) process 11,629 MB
Ubuntu 26.04 (WebKitGTK 2.52.3) web process 6,262 MB
Windows 11 (WebView2 151) not affected host flat at 65–77 MB

The two WebKit ports mirror each other in which process holds the memory — watching only one of them shows nothing. Chromium doesn't have the failure mode at all.

Fixes #4587

It's payload size, not call rate

A step function of per-event payload size, not of how many events you send:

payload (const 4 MB/s) 1 KB 2 KB 4 KB 8 KB 16 KB 32 KB 64 KB
host retention 0.9% 0.0% 0.0% 0.0% 128% 149% 124%

Below the knee there is no retention at all — 1,191,895 events at 64 B (240 s at 5000 ev/s) left the content process at its own floor, ending lower than it started. macOS flips between 8–16 KB; WebKitGTK between 64–128 KB.

Mechanism

vmmap during a leaking run — every mapped region flat or shrinking, all growth in owned unmapped (memory the process owns on the ledger but has unmapped: the sender's side of a shared-memory handoff):

region snap 1 snap 3
owned unmapped 162.3 MB (1,982 regions) 634.1 MB (7,740 regions)
MALLOC_SMALL 13.3 MB 8.9 MB
WebKit Malloc 3.0 MB 2.4 MB

5,758 new regions for 5,760 events — one retained region per oversized eval.

Ruled out by experiment: an undelivered IPC backlog (max lag 26 events against 11.5 GB), a missing @autoreleasepool (patched it; no change), and Wails' own CString/NSString (removing only the eval call while keeping both holds the host at 53 MB vs 11,675 MB). Go heap stays flat at 24 MB throughout.

The change

Events whose marshalled JSON exceeds maxInlineEventPayload (8192) are parked host-side under a random 128-bit id and fetched by the webview over the asset server, which delivers via a URL scheme task and never touches the eval IPC path. Small events keep the existing inline path byte-for-byte.

before after
macOS size-1MB 11,629 MB 65.8 MB 177×
macOS iso-64KB 1,004 MB 56.5 MB 18×
Linux size-1MB 6,262 MB 225 MB 28×

owned unmapped disappears from the region table entirely; host footprint holds flat at 33.8 MB across the run. Linux throughput improves 91 → 100 ev/s.

Ordering is preserved. Once a window sends one event by reference it routes all subsequent events through the same JS promise chain, so a small event cannot overtake a large one still being fetched. Verified with an interleave scenario alternating 1 KB and 64 KB — consecutive sequence numbers travelling by different mechanisms — at 0 reorders, 0 drops on both platforms.

No public API change and no runtime JS change. All three delivery templates call the same window._wails.dispatchWailsEvent and keep the existing guard for the runtime not yet being mounted.

Store hygiene: one-shot on read, bound to the dispatching window via the existing x-wails-window-id header, TTL-reaped (sweeping at TTL/4 so the 30 s bound actually holds), 64 MB cap with fallback to inline on overflow, released on window destroy, and the reaper stopped via OnShutdown.

Not fixed here

dispatchOnMainThread runs inline when already on the main thread, so a main-thread emit can overtake a queued goroutine emit. Pre-existing, unrelated to payload size, unaffected by this change. The mixedsource scenario is included as its regression test.

Correction (added after merge). This section originally cited "~4.4% inversions on master (2,640 of 59,969 events)". That figure was wrong and should not be relied on. The mixedsource scenario took its sequence number outside the InvokeAsync callback and dispatched later, so sequence order differed from call order by construction — the reorders it counted were the harness racing itself, not the framework.

The underlying bug is real, and with corrected methodology the finding is stronger: once emit order is actually well defined, the unfixed code does not merely reorder, it deadlocks — a goroutine holds the emit lock while parked in InvokeSync waiting for the UI thread. The deterministic proof is a unit test that drives DispatchWailsEvent and delivers "second" before "first" without a queue.

Scenario fix, the ordering fix itself, and measurements on all three platforms: #5934.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • WEP (proposal only; no implementation)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

New harness at v3/tests/event-performance — a standalone Wails app that drives events straight at DispatchWailsEvent, samples both the host and web content processes every 250 ms, and grades on floor rise (min of second half − min of first half) rather than a regression slope, since memory under GC is a sawtooth and a slope mostly reports which phase the window opened in.

go run ./tests/event-performance -duration 30s

Roughly 1.9 M events on macOS and 1.2 M on Linux across the full matrix, plus the before/after and attribution runs.

  • Windows
  • macOS
  • Linux

Linux: Ubuntu 26.04 LTS, x86_64, webkit2gtk-4.1 2.52.3 (linked GTK4 / webkitgtk-6.0), headless under Xvfb, Go 1.26.2.
Windows: Windows 11 Pro 26200, x86_64, WebView2 runtime 151.0.4129.72, Go 1.26.2, MinGW-w64 gcc 15.2, run in the interactive console session.

WebView2 does not have this bug

The host process holds flat at 65–77 MB unpatched, where macOS reached 11,629 MB. The content process sawtooths (climb to ~4 GB, collapse, repeat) in both patched and unpatched builds, so that is V8 reclaiming the 1 MB objects the events genuinely carry, not transport retention. WebKitGTK by contrast climbed monotonically to 6.2 GB and never recovered.

The change still helps on Windows, for a different reason — isolated 90 s runs, fresh process each:

achieved content-process accumulation
unpatched 67 ev/s ~80 MB/s
patched 82 ev/s ~43 MB/s

More bytes delivered at roughly half the accumulation rate, which fits: an eval materialises both the 1 MB source string and the parsed object, a fetch only the object. Throughput is the firmer number; the ~2× is one run against a 4 GB sawtooth and should be read as directional.

Honest cost on Windows: the host runs higher patched (146–233 MB vs 65–77 MB) from payloads parked awaiting fetch plus Go GC lag. Bounded, but a real trade.

Note the 28-scenario matrix is not usable on Chromium — V8 never returns to a stable floor, so growth carries across scenarios and per-scenario attribution breaks down. Only isolated fresh-process runs are reported for Windows. The floor-rise metric is calibrated to WebKit and needs rethinking for Chromium.

Test Configuration

Wails v3.0.0-dev › Wails Doctor
# System
┌──────────────────────────────┐
| Name          | MacOS        |
| Version       | 26.4.1       |
| ID            | 25E253       |
| Branding      | MacOS 26.4.1 |
| Platform      | darwin       |
| Architecture  | arm64        |
| Apple Silicon | true         |
| CPU           | Apple M4 Max |
| CPU           | Apple M4 Max |
| GPU           | 32 cores     |
| Memory        | 36 GB        |
└──────────────────────────────┘
# Build Environment
┌──────────────────────────────────────────────────────────────────────┐
| Wails CLI      | v3.0.0-dev                                          |
| Go Version     | go1.26.2                                            |
| -buildmode     | exe                                                 |
| -compiler      | gc                                                  |
| CGO_CFLAGS     |                                                     |
| CGO_CPPFLAGS   |                                                     |
| CGO_CXXFLAGS   |                                                     |
| CGO_ENABLED    | 1                                                   |
| CGO_LDFLAGS    |                                                     |
| DefaultGODEBUG | cryptocustomrand=1,tlssecpmlkem=0,urlstrictcolons=0 |
| GOARCH         | arm64                                               |
| GOARM64        | v8.0                                                |
| GOOS           | darwin                                              |
└──────────────────────────────────────────────────────────────────────┘
# Dependencies
┌──────────────────────────────────────────────────────────────────────────────────────┐
| *Android NDK            | /Users/admin/Library/Android/sdk/ndk/26.3.11579264         |
| *Android SDK            | /Users/admin/Library/Android/sdk                           |
| *Android platform-tools | Installed                                                  |
| *Java (Android)         | openjdk version "21.0.11" 2026-04-21                       |
| *NSIS                   | Not Installed. Install with `brew install makensis`.       |
| *Xcode (iOS)            | Xcode 26.6, Build version 17F113                           |
| *iOS Device SDK         | 26.5                                                       |
| *iOS Simulator SDK      | 26.5                                                       |
| Xcode cli tools         | 2416                                                       |
| npm                     | 10.8.2                                                     |
| docker                  | *Docker version 29.4.0, build 9d7ad9f (daemon not running) |
|                                                                                      |

Checklist:

  • (v2 only) I have updated website/src/pages/changelog.mdx with details of this PR (v3 changelog entries are added automatically)
  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • New Features

    • Large event payloads are delivered reliably through temporary, window-specific retrieval requests.
    • Event ordering is preserved across inline and separately retrieved payloads.
    • Temporary event data expires automatically and is removed when its window closes.
    • The MCP server now starts automatically with the application.
  • Bug Fixes

    • Event delivery falls back gracefully when temporary payload storage is unavailable.
  • Tests

    • Added cross-platform performance testing for throughput, latency, ordering, memory usage, and burst workloads.

leaanthony and others added 2 commits August 9, 2026 21:53
Go->JS events are delivered by splicing the event JSON into a JavaScript
source string and evaluating it in the webview. Above a size threshold,
WebKit switches from inline IPC message data to an out-of-line
shared-memory transfer, and those regions are retained for as long as the
app keeps emitting.

Measured with v3/tests/event-performance at 100 events/sec of 1MB:

  macOS 26.4.1     host process   ->  11,629 MB
  Ubuntu 26.04     web process    ->   6,262 MB

The two ports differ in which process holds the memory, and in where the
threshold sits (macOS 8-16KB, WebKitGTK 64-128KB), but the mechanism is
the same. On macOS vmmap attributes all of it to "owned unmapped", at
exactly one region per oversized eval. Below the threshold there is no
retention at all: 1.19M events at 64B left the content process at its own
floor, so this is a function of per-event payload size, not call rate.

Events whose marshalled JSON exceeds maxInlineEventPayload are now parked
host-side under a random 128-bit id and fetched by the webview over the
asset server, which delivers via a URL scheme task and never touches the
eval IPC path. Small events keep the existing inline path unchanged.

  macOS  size-1MB   11,629 MB -> 65.8 MB   (177x)
  macOS  iso-64KB    1,004 MB -> 56.5 MB   (18x)
  Linux  size-1MB    6,262 MB -> 225 MB    (28x)

vmmap confirms "owned unmapped" disappears from the region table entirely
and host footprint holds flat at 33.8 MB across the run.

Ordering is preserved: once a window has sent one event by reference it
routes all subsequent events through the same JS promise chain, so a small
event cannot overtake a large one still being fetched. Verified with an
interleave scenario alternating 1KB and 64KB payloads - consecutive
sequence numbers travelling by different mechanisms - at 0 reorders and
0 drops on both platforms.

The payload store is one-shot on read, bound to the dispatching window via
the existing x-wails-window-id header, TTL-reaped at 30s, capped at 64MB
with fallback to inline on overflow, and released on window destroy.

No public API change, and no runtime JS change: all three delivery
templates call the same window._wails.dispatchWailsEvent entry point and
keep the existing guard for the runtime not yet being mounted.

Co-authored-by: superDingda <89772770+superDingda@users.noreply.github.com>
A standalone Wails app under v3/tests/event-performance that measures the
Go->JS event transport and produces a repeatable before/after number.

It drives events straight at WebviewWindow.DispatchWailsEvent, the exact
entry point that performs the eval, so the measurement is of the transport
rather than the runtime's listener plumbing or the app-level fanout. The
page intercepts window._wails.dispatchWailsEvent by pre-creating the
_wails object with an accessor, which survives whichever runtime module
mounts first.

Samples both the host process and the web content process every 250ms.
Watching only one of them misses the bug: on macOS the retention shows up
in the host, on Linux in the web process.

  sampler_darwin.go  proc_pid_rusage ri_phys_footprint; content process
                     located by diffing the pid set across window creation
                     (an empty diff aborts rather than summing every
                     WebContent on the machine)
  sampler_linux.go   Pss via smaps_rollup; WebKitWebProcess located via
                     /proc/*/cmdline, since comm truncates at 15 chars and
                     the name is 16
  sampler_other.go   stubs, so timing and ordering still run elsewhere

Verdicts grade on floor rise - the minimum of the window's second half
minus the minimum of its first - not on a regression slope. Memory under
GC is a sawtooth, so a slope mostly reports which phase the window opened
in, and it produced false leak labels on the first pass. A process that is
genuinely leaking cannot return to its earlier floor. Go MemStats are
recorded alongside, so native growth can be told apart from Go heap.

Scenarios:

  idle          baseline drift with a webview open and no events
  rate-*        10..5000 ev/s at 64B - isolates call count
  size-*        1KB..1MB at 100 ev/s - isolates payload size
  iso-*         ~4MB/s held constant, 1KB..64KB - locates the threshold
  iso2-*        ~32MB/s held constant, 32KB..1MB - for higher knees
  interleave    alternating 1KB/64KB - ordering across delivery modes
  mixedsource   goroutine + main-thread emitters on one sequence
  burst         mean 1000 ev/s in bursts of 20
  pathological  10 ev/s at 8MB

Outputs a CSV per scenario, summary.json, and REPORT.md.

Known result: mixedsource reports ~4.4% inversions on master. That is a
pre-existing hazard - dispatchOnMainThread runs inline when already on the
main thread, so a main-thread emit overtakes a queued goroutine emit - and
is unrelated to event payload size. Recorded here as a regression test for
whoever fixes it.
Copilot AI lite review requested due to automatic review settings August 9, 2026 11:54
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The PR adds bounded host-side storage for oversized events, one-shot window-bound retrieval, ordered JavaScript delivery, and a cross-platform event-performance harness with scenario execution, memory sampling, and report generation.

Changes

Event delivery and performance measurement

Layer / File(s) Summary
Bounded event payload storage
v3/pkg/application/event_payload_store.go
Stores oversized payloads with random IDs, size limits, window ownership, one-shot retrieval, TTL cleanup, and shutdown handling.
Window-bound event delivery
v3/pkg/application/application.go, v3/pkg/application/webview_window.go
Routes payload requests through asset handling. Oversized events use fetch references. Promise chaining preserves event order. Window destruction removes stored payloads. Application startup also starts the MCP server.
Performance scenario model
v3/tests/event-performance/scenarios.go
Defines rate, payload-size, ordering, mixed-source, burst, and large-payload scenarios.
Event-performance execution
v3/tests/event-performance/main.go, v3/tests/event-performance/assets/index.html
Runs scenarios, emits events, records JavaScript delivery data, samples process state, and writes results.
Metrics, verdicts, and platform sampling
v3/tests/event-performance/report.go, v3/tests/event-performance/sampler_*.go
Computes delivery, latency, throughput, memory, retention, and validity metrics. Generates CSV, JSON, Markdown, and console reports.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GoHarness
  participant WebviewWindow
  participant eventPayloadStore
  participant JavaScriptRuntime
  participant ReportEndpoint
  participant ReportGenerator
  GoHarness->>WebviewWindow: Emit configured event scenarios
  WebviewWindow->>eventPayloadStore: Store oversized event payload
  WebviewWindow->>JavaScriptRuntime: Deliver inline or fetch-reference event
  JavaScriptRuntime->>eventPayloadStore: Fetch referenced payload
  JavaScriptRuntime->>ReportEndpoint: Send delivery and frame metrics
  GoHarness->>ReportGenerator: Provide emission, memory, and process samples
  ReportEndpoint-->>ReportGenerator: Provide JavaScript reports
  ReportGenerator-->>GoHarness: Write verdicts and scenario reports
Loading

Possibly related PRs

Suggested labels: runtime, size:L

Poem

A rabbit stores each payload tight,
Then sends events in ordered flight.
The harness measures every stream,
While reports track each memory dream.
Clean shutdown ends the night.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses large-payload memory retention but does not fully implement the issue's requested eval-only notification and fetch model for all payloads. Extend payload delivery so eval sends only notifications and all event data uses the fetch model, or split the remaining small-payload work into a linked follow-up issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: oversized event payloads no longer use evaluateJavaScript.
Description check ✅ Passed The description covers motivation, issue linkage, implementation, testing, configuration, limitations, and checklist status.
Out of Scope Changes check ✅ Passed The event-performance harness, platform samplers, and reporting code directly support validation of the event payload memory fix.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/oversized-event-payloads

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread v3/pkg/application/application.go Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (6)
v3/tests/event-performance/sampler_other.go (1)

5-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update the stale comment.

The build tag excludes darwin and linux, but the comment states "unsupported off darwin". Linux sampling now exists in sampler_linux.go.

✏️ Proposed change
-// Memory metrics are unsupported off darwin, but the timing and ordering half
-// of the harness still runs so engines can be compared later.
+// Memory metrics are unsupported outside darwin and linux, but the timing and
+// ordering half of the harness still runs so engines can be compared later.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/tests/event-performance/sampler_other.go` around lines 5 - 8, Update the
comment above samplerSupported in sampler_other.go to reflect that memory
metrics are unsupported only on platforms other than Darwin and Linux, while
timing and ordering comparisons still run there.
v3/tests/event-performance/scenarios.go (2)

170-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace itoa with strconv.Itoa.

The hand-written itoa duplicates the standard library and returns an empty string for negative input. strconv.Itoa is correct for all values.

♻️ Proposed replacement
-func itoa(i int) string {
-	if i == 0 {
-		return "0"
-	}
-	var b [20]byte
-	p := len(b)
-	for i > 0 {
-		p--
-		b[p] = byte('0' + i%10)
-		i /= 10
-	}
-	return string(b[p:])
-}

Then use strconv.Itoa(r) at Line 60 and import "strconv".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/tests/event-performance/scenarios.go` around lines 170 - 182, Remove the
custom itoa function and use strconv.Itoa for integer-to-string conversion,
including replacing the call that converts r. Add the strconv import and
preserve the existing output for all integer values, including negatives.

152-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Report unknown scenario names in -only.

selectedScenarios drops names that do not match a scenario. If the user mistypes a name, the harness runs zero scenarios and still writes an empty report. Return an error or log the unmatched names.

♻️ Proposed change to surface unmatched names
 	var out []Scenario
 	for _, s := range all {
 		if want[s.Name] {
 			out = append(out, s)
+			delete(want, s.Name)
 		}
 	}
+	for n := range want {
+		log.Printf("unknown scenario name in -only: %q", n)
+	}
 	return out
 }

Add "log" to the imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/tests/event-performance/scenarios.go` around lines 152 - 168, Update
selectedScenarios to detect names from only that do not match any Scenario in
allScenarios, and surface each unmatched name through the established error or
logging path instead of silently returning no scenarios. Preserve matching and
empty-input behavior.
v3/tests/event-performance/main.go (1)

464-477: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the platform process name in the error message.

discoverWebContent is shared by all platforms, but the error text names com.apple.WebKit.WebContent. On Linux the harness looks for WebKitWebProcess. Both samplers already export webContentProcName, so use that constant.

♻️ Proposed fix
-		return fmt.Errorf("no new com.apple.WebKit.WebContent process appeared after window creation "+
-			"(%d existed before launch); cannot attribute one to this app", len(h.basePids))
+		return fmt.Errorf("no new %s process appeared after window creation "+
+			"(%d existed before launch); cannot attribute one to this app",
+			webContentProcName, len(h.basePids))

sampler_other.go must then also declare webContentProcName.

Based on learnings that platform-specific behavior in shared files should be controlled at compile time rather than hardcoded for one OS.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/tests/event-performance/main.go` around lines 464 - 477, Update
discoverWebContent to use the shared platform-specific webContentProcName
constant in its error message instead of hardcoding com.apple.WebKit.WebContent.
Ensure sampler_other.go also declares webContentProcName with the Linux process
name WebKitWebProcess, matching the existing declaration used by the other
sampler.

Source: Learnings

v3/tests/event-performance/report.go (2)

356-375: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Gate the macOS-only environment probes.

sw_vers, machdep.cpu.brand_string, and the WebKit Info.plist read exist on macOS only. On Linux the harness spawns three processes per report and records nothing. Split this function per build tag, or read /proc/cpuinfo and /etc/os-release on Linux.

Based on learnings that platform-specific behavior should be controlled via build tags rather than executed unconditionally in shared files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/tests/event-performance/report.go` around lines 356 - 375, Update
environment() so macOS-specific probes (sw_vers, machdep.cpu.brand_string, and
the WebKit Info.plist read) are compiled only for macOS via build-tagged
platform-specific implementations. Keep the shared environment values and git
commit collection available across platforms, and provide the Linux
implementation using appropriate Linux system sources such as /proc/cpuinfo and
/etc/os-release.

Source: Learnings


72-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Check gofmt changes for these event-performance files.

gofmt -l lists v3/tests/event-performance/report.go and v3/tests/event-performance/scenarios.go as requiring formatting. Update these files so repository formatting checks do not fail.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/tests/event-performance/report.go` around lines 72 - 82, Run gofmt on
report.go and scenarios.go, applying the formatter’s alignment and spacing
changes throughout both event-performance files. Preserve all existing behavior
and values; only update formatting so gofmt -l no longer reports either file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v3/pkg/application/application.go`:
- Line 75: In New, immediately after initializing result.eventPayloads with
newEventPayloadStore(), register result.eventPayloads.close through
result.OnShutdown so the reap goroutine is stopped during application shutdown.

In `@v3/pkg/application/event_payload_store.go`:
- Around line 127-139: Update the cleanup loop in the event payload store to
sweep more frequently than the 30-second TTL, and change its expiration
condition to treat entries whose deadline equals now as expired using
item.created.Add(eventPayloadTTL).After(now). Preserve the existing deletion and
byte-accounting behavior.

In `@v3/pkg/application/webview_window.go`:
- Around line 1420-1428: Serialize event mode selection, payload storage, and
JavaScript enqueueing in the window event-dispatch path using a per-window mutex
or shared ordered dispatcher. Update the logic around eventRefMode and ExecJS so
concurrent inline and reference-mode events cannot interleave during the
transition, preserving event order for all subsequent events.

In `@v3/tests/event-performance/report.go`:
- Around line 174-176: Update the ByteRatePerSec calculation in the report
generation logic to account for both Payload and AltPayloadBytes, using the
effective or average payload size for interleave scenarios. Preserve the
existing Sent and secs scaling so the reported rate reflects all transmitted
bytes.

In `@v3/tests/event-performance/sampler_darwin.go`:
- Around line 12-14: Update the Darwin process-count handling in webContentPids
to use the value returned by hp_listpids directly as the pid count; remove the
extra division by sizeof(C.int) while preserving the existing scan and
discoverWebContent behavior.

---

Nitpick comments:
In `@v3/tests/event-performance/main.go`:
- Around line 464-477: Update discoverWebContent to use the shared
platform-specific webContentProcName constant in its error message instead of
hardcoding com.apple.WebKit.WebContent. Ensure sampler_other.go also declares
webContentProcName with the Linux process name WebKitWebProcess, matching the
existing declaration used by the other sampler.

In `@v3/tests/event-performance/report.go`:
- Around line 356-375: Update environment() so macOS-specific probes (sw_vers,
machdep.cpu.brand_string, and the WebKit Info.plist read) are compiled only for
macOS via build-tagged platform-specific implementations. Keep the shared
environment values and git commit collection available across platforms, and
provide the Linux implementation using appropriate Linux system sources such as
/proc/cpuinfo and /etc/os-release.
- Around line 72-82: Run gofmt on report.go and scenarios.go, applying the
formatter’s alignment and spacing changes throughout both event-performance
files. Preserve all existing behavior and values; only update formatting so
gofmt -l no longer reports either file.

In `@v3/tests/event-performance/sampler_other.go`:
- Around line 5-8: Update the comment above samplerSupported in sampler_other.go
to reflect that memory metrics are unsupported only on platforms other than
Darwin and Linux, while timing and ordering comparisons still run there.

In `@v3/tests/event-performance/scenarios.go`:
- Around line 170-182: Remove the custom itoa function and use strconv.Itoa for
integer-to-string conversion, including replacing the call that converts r. Add
the strconv import and preserve the existing output for all integer values,
including negatives.
- Around line 152-168: Update selectedScenarios to detect names from only that
do not match any Scenario in allScenarios, and surface each unmatched name
through the established error or logging path instead of silently returning no
scenarios. Preserve matching and empty-input behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b5634a5-edad-4e6c-ab36-2fd846f0c6be

📥 Commits

Reviewing files that changed from the base of the PR and between be29bfb and fe79741.

📒 Files selected for processing (10)
  • v3/pkg/application/application.go
  • v3/pkg/application/event_payload_store.go
  • v3/pkg/application/webview_window.go
  • v3/tests/event-performance/assets/index.html
  • v3/tests/event-performance/main.go
  • v3/tests/event-performance/report.go
  • v3/tests/event-performance/sampler_darwin.go
  • v3/tests/event-performance/sampler_linux.go
  • v3/tests/event-performance/sampler_other.go
  • v3/tests/event-performance/scenarios.go

Comment thread v3/pkg/application/application.go
Comment thread v3/pkg/application/event_payload_store.go Outdated
Comment thread v3/pkg/application/webview_window.go
Comment thread v3/tests/event-performance/report.go
Comment thread v3/tests/event-performance/sampler_darwin.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes Wails v3 Go→JS event delivery to avoid WebKit’s out-of-line shared-memory retention when oversized event JSON is spliced into evaluateJavaScript source. Large event payloads are now parked host-side and fetched by the webview via the asset server, while small events keep the existing inline eval path; a new v3/tests/event-performance harness is added to measure memory/ordering/timing.

Changes:

  • Route oversized event payloads through a host-side payload store + asset-server fetch (small payloads remain inline).
  • Preserve in-order delivery by chaining dispatch through a per-window JS promise once any by-reference event is used.
  • Add a cross-platform event dispatch performance harness (v3/tests/event-performance) with memory samplers for darwin/linux.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
v3/pkg/application/webview_window.go Adds inline/chained/ref delivery templates and switches oversized events to by-reference fetch.
v3/pkg/application/event_payload_store.go Introduces TTL/capped store for oversized event payloads keyed by random IDs.
v3/pkg/application/application.go Hooks /wails/eventpayload/… into the asset server to serve one-shot payload fetches; stores the payload store on App.
v3/tests/event-performance/main.go Adds the event dispatch benchmark harness app and scenario runner.
v3/tests/event-performance/report.go Computes/exports CSV + summary.json + markdown report with floor-rise grading.
v3/tests/event-performance/scenarios.go Defines the scenario matrix (rate sweep, size sweep, iso-byte-rate, ordering probes).
v3/tests/event-performance/assets/index.html Adds a runtime interception page to measure drops/reorders/latency and report to Go.
v3/tests/event-performance/sampler_darwin.go Adds macOS WebContent PID discovery + footprint sampling via libproc.
v3/tests/event-performance/sampler_linux.go Adds Linux WebKitWebProcess PID discovery + PSS/RSS sampling via /proc.
v3/tests/event-performance/sampler_other.go Adds a stub sampler for unsupported platforms (timing/ordering only).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread v3/pkg/application/webview_window.go Outdated
Comment on lines 74 to 76
result.customEventProcessor = NewWailsEventProcessor(result.Event.dispatch)
result.eventPayloads = newEventPayloadStore()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — OnShutdown(eventPayloads.close) is registered where the store is created, so the reaper doesn't outlive App.cleanup(). A follow-up review also found that close racing put could strand a payload and that concurrent close could panic; both are handled now with a mutex-guarded closed flag and a sync.Once. (5ed70d3, 283141790)

Comment thread v3/pkg/application/application.go
Comment thread v3/tests/event-performance/sampler_darwin.go Outdated
Comment thread v3/pkg/application/event_payload_store.go
Three findings from automated review:

- CodeQL flagged the ParseUint -> uint conversion for the window id header
  as truncating on 32-bit builds. Parse at strconv.IntSize so the value
  cannot exceed uint width.

- The store owns a reaper goroutine, but nothing stopped it: window close
  only calls dropWindow, and close was never wired up. Register it via
  OnShutdown alongside store initialisation.

- The reaper swept once per TTL, so an entry created just after a sweep
  survived until the following one - holding it for nearly 60s against a
  documented 30s bound. Sweep at TTL/4 and treat an entry as expired when
  its deadline is reached rather than strictly passed.
WebView2 has no single content process: an app gets a browser process plus
renderer, GPU and utility children, all named msedgewebview2.exe, and a dev
box typically has a dozen belonging to other apps already running. The
existing pre-launch pid diff handles that - every pid appearing after our
window is created is ours - so the sampler only has to enumerate and
measure.

Memory is reported as PrivateUsage from GetProcessMemoryInfo, the closest
Windows analogue to phys_footprint: private committed bytes, excluding the
mapped images every WebView2 child shares, which would otherwise be
counted once per process.
Windows 11 / WebView2 151 does not have the retention: the host process
holds flat at 65-77 MB where macOS reached 11.6 GB, and the content
process sawtooths under V8 GC identically in patched and unpatched
builds. So 8192 is correct on all three engines and conservative off
macOS rather than unverified.
@leaanthony
leaanthony force-pushed the fix/oversized-event-payloads branch from 5ed70d3 to b086266 Compare August 9, 2026 13:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v3/pkg/application/event_payload_store.go`:
- Around line 159-165: Add mutex-protected shutdown state to eventPayloadStore
and update close and put so shutdown and payload insertion are serialized: close
must mark the store closed before stopping the janitor, and put must return
false without storing anything once shutdown has started. Replace the
select-and-close logic in close with a dedicated sync.Once to make concurrent
shutdown calls safe, while preserving normal insertion before shutdown.

In `@v3/tests/event-performance/sampler_windows.go`:
- Around line 49-59: Update webContentPids to return and propagate snapshot and
Process32First errors instead of returning nil as an empty process list. At its
callers in main.go, treat enumeration failure as invalid memory sampling or stop
the Windows run, preventing failed pre-launch enumeration from being used as a
valid baseline.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f41dfbf2-3fda-477b-90b3-6edc312c2467

📥 Commits

Reviewing files that changed from the base of the PR and between 5ed70d3 and b086266.

📒 Files selected for processing (3)
  • v3/pkg/application/event_payload_store.go
  • v3/tests/event-performance/sampler_other.go
  • v3/tests/event-performance/sampler_windows.go

Comment thread v3/pkg/application/event_payload_store.go Outdated
Comment thread v3/tests/event-performance/sampler_windows.go
Ordering race (CodeRabbit): a small event could read eventRefMode as false
while a large event sat between store.put and the flag being set, then
dispatch inline and overtake it. Fixed, but not with a per-window mutex as
suggested - that would have to be held across the ExecJS main-thread hop,
which is the same shape as the deadlock documented in
transport_event_ipc.go. Instead the decision moves into JavaScript: evals
execute serially on the UI thread in enqueue order, so testing w.__eq there
is race-free by construction and needs no Go-side state. eventRefMode is
gone.

Unterminated promise chain (Copilot): the chained template had no catch, so
one throwing listener would leave w.__eq permanently rejected and silently
stop all later events for that window. Both templates now terminate with
catch.

close/put race (CodeRabbit): put could store a payload after close, leaving
it unreapable, and concurrent close could panic on a double channel close.
Added a mutex-guarded closed flag that makes put refuse, and a dedicated
sync.Once around the channel close.

Endpoint hardening (Copilot): restrict to GET/HEAD and validate the id is
exactly 32 hex characters before touching the map.

proc_listallpids returns a PID COUNT, not a byte count (CodeRabbit and
Copilot, independently). Verified empirically on macOS: 907 returned
against 907 pids present. Dividing by sizeof(int) meant scanning a quarter
of the process table, which could have missed our own content process and
produced a spurious "no new WebContent process" abort. It found the pid
anyway in every run so far, by luck.

Enumeration failures no longer look like an empty process list
(CodeRabbit): webContentPids returns an error on all platforms, a failed
pre-launch enumeration is now fatal rather than silently classifying every
pre-existing content process as ours, and discovery propagates the error.

ByteRatePerSec ignored AltPayloadBytes (CodeRabbit), understating the
interleave scenario's byte rate ~32x in the column that carries the
attribution signal. Now averages the two sizes.

Verified after the changes: 224,115 events at 4980/s with 0 drops and
0 reorders, emit p50 42us (unchanged); interleave 0 reorders across mixed
inline and by-reference delivery; size-1MB host peak 54 MB.
Two things. My eventPayloads field broke the alignment of the surrounding
struct fields, which is mine to fix. The rest is a pre-existing mis-indent
of the MCP server block that gofmt corrects; whitespace only, no
semantic change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v3/tests/event-performance/sampler_other.go`:
- Around line 7-8: Update the comment above the fallback in the sampler harness
to describe platforms without a sampler, replacing the Darwin-specific “off
darwin” wording. Preserve the existing explanation that timing and ordering
still run for engine comparisons.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f218e116-51ca-44b2-b2e4-d7d4f7ac2ece

📥 Commits

Reviewing files that changed from the base of the PR and between b086266 and d52af65.

📒 Files selected for processing (10)
  • v3/pkg/application/application.go
  • v3/pkg/application/event_payload_store.go
  • v3/pkg/application/webview_window.go
  • v3/tests/event-performance/main.go
  • v3/tests/event-performance/report.go
  • v3/tests/event-performance/sampler_darwin.go
  • v3/tests/event-performance/sampler_linux.go
  • v3/tests/event-performance/sampler_other.go
  • v3/tests/event-performance/sampler_windows.go
  • v3/tests/event-performance/scenarios.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • v3/tests/event-performance/sampler_linux.go
  • v3/tests/event-performance/sampler_darwin.go
  • v3/tests/event-performance/scenarios.go
  • v3/tests/event-performance/main.go
  • v3/pkg/application/event_payload_store.go
  • v3/pkg/application/application.go

Comment thread v3/tests/event-performance/sampler_other.go Outdated
It said metrics are unsupported off darwin, which stopped being true once
linux and windows samplers were added. It is the fallback for platforms
that have no sampler, not for everything that is not darwin.
leaanthony added a commit that referenced this pull request Aug 9, 2026
Three problems in the asset server, found while investigating Go→JS event dispatch (#5930) and independent of it.

contentTypeSniffer.Flush delegated to the wrapped writer without completing itself. Until 512 bytes have been seen the sniffer holds the body back to detect a Content-Type, so a flush before that point sent nothing and reported success — every streaming format silently delivered nothing. Flush now completes first, and complete advances the prefix only by the bytes the writer accepted, recording any error so it surfaces from the next Write or complete since http.Flusher cannot report one.

The platform response writers did not implement http.Flusher, so http.ResponseController(w).Flush() could never reach them. macOS, iOS and Linux now declare a documented no-op — Write already pushes to the URL scheme task or into the pipe WebKitGTK reads. Windows deliberately does not: its Write accumulates into an in-memory buffer handed to WebView2 only in Finish, so a no-op there would claim a capability that does not exist. Leaving it unimplemented keeps ResponseController.Flush returning ErrNotSupported, which is true — and means streaming cannot work on Windows today regardless of WebView2.

dispatchWorkers is declared and read but never assigned, which reads like an oversight and is not: it must stay 0. Any positive value switches to a fixed worker pool where a long-lived request holds its worker for its lifetime, starving later requests — including the page's own assets — and presenting as an unexplained startup hang. Documented rather than removed.

Eight tests cover the sniffer; three fail without the fix.
@leaanthony
leaanthony merged commit 684d1ce into master Aug 9, 2026
48 checks passed
@leaanthony
leaanthony deleted the fix/oversized-event-payloads branch August 9, 2026 14:48
leaanthony pushed a commit that referenced this pull request Aug 9, 2026
…ed event payloads out of evaluateJavaScript
leaanthony added a commit that referenced this pull request Aug 10, 2026
App.dispatchOnMainThread runs work inline when the caller is already on the UI thread, so an event emitted from the UI thread executed its eval immediately while an event emitted moments earlier from a goroutine was still queued — and the later event arrived first.

DispatchWailsEvent now appends to a per-window queue and a single drain on the UI thread empties it in order. Appending under a mutex makes the queue the one ordering authority, whichever thread each event came from.

A lock around the dispatch instead would deadlock: it would have to be held across the ExecJS main-thread hop, so a UI-thread emit would block on a lock whose holder is waiting for the UI thread. The queue avoids that because the bound applies to goroutine emitters only — the UI thread is the drainer and is never made to wait.

Capacity 64, chosen by measurement. Ordering held and 5000 ev/s was sustained at every size tried, so only tail latency moved, rising with depth (179us p99 at 16, 427us at 1024). Emit also stopped blocking on a main-thread round trip, so emit p50 at 5000 ev/s fell from ~45us to 1-2us.

Nothing is dropped when the queue is full: goroutine emitters block until a slot frees, the UI thread appends past the bound and drains inline, and a window destroyed while an emitter waits releases it. Note the bound limits this queue rather than total in-flight memory, since the mailbox added in #5851 is unbounded and sits upstream.

Verified on macOS, Ubuntu 26.04 (both the default GTK4 backend and legacy -tags gtk3) and Windows 11 (WebView2): 0 inversions across mixed-source, interleaved-size and 5000 ev/s runs. Six unit tests need no GUI so they run in CI; the headline one drives DispatchWailsEvent and fails without the queue.

Also corrects the harness scenario that first reported this, which took its sequence number outside the InvokeAsync callback and so measured its own race. The ~4.4% figure quoted in #5930 is retracted.

Prior art: @DevLumuz raised event ordering first in #5757 and is credited on #5851 for the layer that fix addressed. The GTK3 concern from that PR was measured here at 186,047 events with 0 reorders and is not reproduced.
@taliesin-ai taliesin-ai added this to the v3.0.0-beta.3 milestone Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

[v3] Use eval only for event notifications, implement event payload delivery via JS→Go pull model to prevent memory leaks

4 participants