Compare commits

..
Author SHA1 Message Date
Levi cc96cff2ea Merge PR #1583: optimize extension API loading
# Conflicts:
#	ui/flutter/lib/features/extensions/presentation/widgets/extension_detail_view.dart
2026-10-10 17:40:56 +08:00
Levi 15738644b0 perf(extensions): load store summaries without waiting for update checks 2026-10-10 17:32:43 +08:00
Levi 44a5f9eefc Merge PR #1582: widen macOS navigation rail 2026-10-10 14:43:06 +08:00
Levi c90c690836 Merge PR #1581: synchronize extension cookie range coverage
# Conflicts:
#	ui/flutter/test/widget_test.dart
2026-10-10 14:43:04 +08:00
Levi dee1c2515f Merge PR #1535: keep extension update chip within card header 2026-10-10 14:42:53 +08:00
Levi bbf1a9fd7d fix(ui): widen navigation rail for macOS traffic lights 2026-10-10 14:34:11 +08:00
Levi b3f462bb7d test(http): synchronize ignored Range response coverage 2026-10-09 21:15:51 +08:00
Levi 0d08004c49 revert: remove macOS traffic light adjustments 2026-10-09 21:13:55 +08:00
Levi 1589d4cbe7 fix: update macOS traffic lights through window events 2026-10-09 20:58:24 +08:00
Levi b6ce7756fb fix: keep macOS traffic lights within the navigation rail 2026-10-09 20:49:14 +08:00
Levi 342c55fbd6 fix: use server-side fetch header permissions for extensions 2026-10-09 19:59:06 +08:00
Levi fe2f45e40d fix: preserve explicit Cookie headers in extension requests 2026-10-09 19:39:05 +08:00
Levi 2d4a8cddc1 fix(ui): keep extension update chip within card header 2026-09-26 11:05:43 +08:00
Levi 76c22c50d8 test: align beta2 integration checks 2026-09-25 16:36:27 +08:00
Levi f400137ead Merge PR #1542 into 2.0.0-beta.2 2026-09-25 16:23:55 +08:00
Levi a5ebbca318 Merge PR #1536 into 2.0.0-beta.2
# Conflicts:
#	ui/flutter/test/widget_test.dart
2026-09-25 16:23:55 +08:00
Levi 8f2af5660e Merge PR #1535 into 2.0.0-beta.2 2026-09-25 16:23:39 +08:00
Levi 932a49c1d1 Merge PR #1532 into 2.0.0-beta.2 2026-09-25 16:23:39 +08:00
Levi 212c735b66 Merge latest main into 2.0.0-beta.2 2026-09-25 16:23:39 +08:00
ashish6298 b5e8d61f01 fix: move checksum to HTTP opts, fix mismatch retry, scope get_download_speed to aggregate-only 2026-09-25 12:15:50 +05:30
ashish6298 929e2d2d7c test(base): add unit tests for ChecksumOption serialization 2026-09-22 16:36:41 +05:30
ashish6298 e93437c95f feat: implement Model Context Protocol (MCP) server and download manager capabilities 2026-09-22 16:16:01 +05:30
Levi 7bc7efed5b feat(task): show a dash for completed tasks without a done time
Tasks completed before this change have no doneAt value; the drawer now
always renders the Completed at row for completed tasks and falls back
to a dash instead of hiding the row.
2026-09-20 09:28:46 +08:00
Levi 1d8a502ac4 fix(task): keep DoneAt once written instead of clearing it
Done tasks never leave the done state (doStart, pause and markTaskError
all guard against Done), so the else branch clearing DoneAt was
unreachable dead code. DoneAt is now written once on completion and
kept for the task lifetime.
2026-09-20 09:21:36 +08:00
Levi 4b231f52c5 feat(task): record and display task completion time
Add DoneAt to the download task model: set when a task finishes and
cleared when it is downloaded again. The value flows through the API
model and task record, and is shown as "Completed at" in the task
drawer info tab next to the creation time.
2026-09-20 08:56:54 +08:00
Levi e956bd1ce5 style(ui): tighten card update chip spacing to 4px 2026-09-20 08:05:00 +08:00
Levi 68402c85d3 revert(ui): keep extension update check silent on failure
A failed check simply means no update chip; the failure toast and its
state field are removed.
2026-09-20 08:02:22 +08:00
Levi a480c5bd14 fix(ui): align extension update dialog title with app upgrade dialog 2026-09-19 22:16:37 +08:00
Levi e779de285d fix(ui): card chip spacing, instant flag clearing, locale-wide label
- Space the update chip off the card title to match the detail header
- Clear the extension's update flag right after a successful upgrade so
  the chip disappears without waiting for the background re-check
- Use the concise Update label across all locales
2026-09-19 21:56:46 +08:00
Levi fac17b0ccc style(ui): shorten update button label to Update
The shared newVersionUpdate label now reads Update on the app upgrade
dialog, the tracker update button and the extension update dialog, so
the extension flow matches the host dialog wording.
2026-09-19 21:47:44 +08:00
Levi 8c728ffda7 style(ui): render the update status as a bordered chip
Pill shape with a brand-green tint, outline and dot so the status
reads as a badge next to the title instead of plain text.
2026-09-19 21:41:56 +08:00
Levi 1b4956b0ec fix(ui): reserve the pointer cursor for the update chip
Extension cards no longer show a pointer cursor on hover; the tappable
update status chip shows it instead, on cards and detail headers alike.
2026-09-19 21:18:57 +08:00
Levi 2e5f8948be fix(ui): restore homepage and GitHub buttons on extension cards 2026-09-19 20:55:22 +08:00
Levi 04ebca60cf refactor(ui): make the status chip the only update entry
Remove the update button from the detail actions; the green chip next
to the extension title opens the update dialog on cards, the desktop
drawer and the mobile details page alike.
2026-09-19 20:49:20 +08:00
Levi 948f6d5487 fix(ui): let the update status chip shrink instead of overflow
Long translations (e.g. Italian, Tamil) could exceed the card title row
on minimum-width cards. The chip is now flex-sized and its label
ellipsizes, so the title and chip degrade gracefully at any width and
locale.
2026-09-19 20:02:28 +08:00
Levi 51b7abed48 refactor(ui): replace update dot with tappable status chip
Card and detail headers now show a green update chip next to the
extension title; on cards it opens the update dialog directly. The red
dot and the red card button are gone, and the status chip no longer
duplicates inside the detail content. Shorten the English label so the
chip fits narrow cards.
2026-09-19 19:47:02 +08:00
Levi d8758267c4 refactor(ui): polish extension update affordance per review
- Move the update dot from the icon corner to beside the card title
- Restyle the card update button to match the row's ghost buttons
- Drop the detail-view dot; the existing update status chip covers it
- Rename the detail update button label to just Update
2026-09-19 19:04:58 +08:00
Levi d94b59b165 refactor(ui): open extension details from every card
Installed extensions missing from the store index previously had no tap
handler on their card, so they kept on-card homepage/GitHub buttons as
the only metadata entry. Make all cards open the detail view and drop
the homepage/GitHub buttons entirely; details remain the single place
for links.
2026-09-19 18:43:57 +08:00
Levi f34cb9c971 refactor(ui): declutter extension card actions
Drop homepage/GitHub buttons from store extension cards (they remain on
the detail page); keep them only for unlisted extensions whose cards do
not open a detail view. With the labeled update button this caps card
actions at update/settings/delete.
2026-09-19 18:07:28 +08:00
Levi 172f4a3650 fix(ui): unify extension update check and improve update affordance
- Update availability is now decided solely by the backend live GitHub
  check (updateFlags); the store index may lag and previously masked
  new versions in the market view.
- Replace the ambiguous refresh icon on extension cards with a labeled
  update button, and add a red dot badge on extension icons (card and
  detail view) when an update is available.
- Surface a toast when the update check fails instead of failing silently.
2026-09-19 17:32:44 +08:00
Levi 085ebf357a fix(ui): smooth task progress bar updates 2026-09-19 00:14:15 +08:00
Levi dae13dec01 Merge latest PR #1527 into 2.0.0-beta.2 2026-09-18 09:43:51 +08:00
Levi a8f682559f Merge latest PR #1523 into 2.0.0-beta.2 2026-09-18 09:43:51 +08:00
Levi 3c0c019bc9 fix(bt): derive cleanup boundaries from torrent storage layout 2026-09-18 09:33:42 +08:00
Levi 69b3e223ad fix(bt): always preserve the torrent root directory 2026-09-18 09:23:46 +08:00
Levi 183616fcc0 fix(webview): suppress GTK fallback error document events 2026-09-18 09:13:40 +08:00
Levi 3415364ab7 test(webview): allow for throttled timers and diagnose selector timeouts 2026-09-18 09:04:39 +08:00
Levi 1b00a16277 fix(webview): update native callback lifetime and test coverage instrumentation 2026-09-18 09:00:50 +08:00
Levi b33c00d90d ci: resolve Linux native crash address in covered test binary 2026-09-18 00:36:41 +08:00
Levi 7e07ea31cc test(webview): cover event stream failures and cleanup 2026-09-18 00:31:06 +08:00
Levi c2da22f371 fix(webview): update GTK lifecycle cleanup and repeat Linux integration tests 2026-09-18 00:21:13 +08:00
Levi 2fd8a96c5d fix(webview): ignore load notifications from browser error documents 2026-09-18 00:07:55 +08:00
Levi 1776b05782 test(webview): trace navigation completion in event contract 2026-09-18 00:03:03 +08:00
Levi ffbae58e6b test(webview): trigger navigation failure consistently through proxies 2026-09-17 23:58:30 +08:00
Levi 2c7a86b21c build(deps): use webview_go with Go 1.17 minimum 2026-09-17 23:52:58 +08:00
Levi 4157e0a5bc fix(deps): update webview_go with legacy Go compatibility fix 2026-09-17 23:51:26 +08:00
Levi dfe8e9a3fd feat(extension): expose host version and WebView page events 2026-09-17 23:43:39 +08:00
Levi 7c33d978c4 fix(bt): clean partial files and empty parent directories 2026-09-17 23:35:40 +08:00
Levi 534fbbb3a2 Merge latest PR #1524 into 2.0.0-beta.2 2026-09-17 23:03:45 +08:00
Levi fdb2768008 Merge latest PR #1523 into 2.0.0-beta.2 2026-09-17 23:03:45 +08:00
Levi 27202cf99e Merge latest PR #1518 into 2.0.0-beta.2 2026-09-17 23:03:45 +08:00
Levi a05eea3268 fix(ui): restore tray menu dismissal and use upstream plugin 2026-09-17 23:02:20 +08:00
Levi 7496331ffd fix(flutter): align notification file actions with task cards 2026-09-17 23:02:14 +08:00
Levi 967ef25408 fix(bt): remove unselected files from the correct download path 2026-09-17 22:47:07 +08:00
Levi d1b2ae42af fix: finish migration to main WebView profile library 2026-09-17 21:48:41 +08:00
Levi adeb7a2895 Merge PR #1526 into 2.0.0-beta.2 2026-09-17 21:47:44 +08:00
Levi edfecefc16 Merge PR #1524 into 2.0.0-beta.2 2026-09-17 21:47:44 +08:00
Levi f7dfc4226b Merge PR #1523 into 2.0.0-beta.2 2026-09-17 21:47:44 +08:00
Levi edb8d3cbdc Merge PR #1520 into 2.0.0-beta.2 2026-09-17 21:47:43 +08:00
Levi c6dd58848d Merge latest PR #1518 into 2.0.0-beta.2 2026-09-17 21:47:43 +08:00
Levi 88dd35cc46 Merge latest main into 2.0.0-beta.2
# Conflicts:
#	internal/webview/README.md
#	ui/flutter/lib/app/rpc/webview_rpc_service.dart
#	ui/flutter/pubspec.lock
#	ui/flutter/pubspec.yaml
2026-09-17 21:47:43 +08:00
Levi 051d4267a5 fix(ui): restore path placeholders and window shortcut 2026-09-17 21:08:30 +08:00
sysy 022fbf633b fix: sanitize archive entry paths before extraction to prevent path traversal
Move path sanitization before directory handling in extractFile so
directory entries with "../" prefixes are rejected the same way file
entries already are. Add the same sanitization to extractSevenZipMultiPart
and both zip extraction loops in the Windows updater, which had no path
validation at all.
2026-09-16 23:31:35 -04:00
Levi 014221a1a7 fix(flutter): drop action buttons from error notifications
A failed download has no usable output file, so error notifications now
carry no buttons at all; only the done event offers open file / open
folder actions.
2026-09-17 09:55:07 +08:00
Levi 6f4635e2eb feat(flutter): add action buttons to desktop download notifications
Terminal task notifications now carry actionable buttons and respond to
clicks instead of being inert:

- Done notifications offer Open File and Open Folder for single-file
  tasks, and Open Folder only for folder tasks (e.g. multi-file
  torrents), following the same rule as the task details page. Error
  notifications offer Open Folder only, since the output is rarely
  usable.
- Clicking the notification body brings the app window back to the
  front (show + focus), covering the close-to-tray case.
- The click target is snapshotted into the notification (Windows: action
  arguments; macOS/Linux: notification payload) so buttons keep working
  after the task record is deleted, and a missing file is logged instead
  of surfacing an OS error dialog.
- macOS resolves buttons through three pre-registered notification
  categories (done single-file / done folder / error); Windows uses the
  actions API of the pinned notification plugin fork; Linux uses
  freedesktop notification actions.
- New openFolder label localized in all 22 ARB catalogs.

Cold-start activation is intentionally out of scope: when the process is
not running, OS activation opens the main window without dispatching the
button action.
2026-09-17 09:49:20 +08:00
Levi a83e3a2b84 Merge remote-tracking branch 'origin/main' into codex/fix-bt-file-selection
# Conflicts:
#	ui/flutter/test/features/tasks/domain/task_record_test.dart
2026-09-17 00:03:36 +08:00
Levi 5bb040eaaf fix(bt): honor selected files when creating resolved tasks 2026-09-17 00:03:03 +08:00
Levi e27c7dddb4 fix(http): close prefetch response when temporary-file setup fails 2026-09-16 23:43:43 +08:00
Levi 5342be16d9 Merge main into codex/tempdir-media-spooling 2026-09-16 23:20:29 +08:00
Levi e0ced260e2 feat: spool media inputs to disk and report producer progress 2026-09-16 23:16:19 +08:00
Levi d41e27234d Merge PR #1518 into 2.0.0-beta.2 2026-09-15 23:06:45 +08:00
Levi c703c52cbc fix(ui): refine narrow layouts and enable tab swipes 2026-09-15 23:01:48 +08:00
Levi b88144c864 Merge PR #1517 into 2.0.0-beta.2
# Conflicts:
#	go.mod
2026-09-15 22:51:56 +08:00
Levi 273850ae3f Merge PR #1516 into 2.0.0-beta.2 2026-09-15 22:51:41 +08:00
Levi 07356033f9 Merge PR #1515 into 2.0.0-beta.2 2026-09-15 22:51:41 +08:00
Levi 166aee81be Merge latest PR #1512 into 2.0.0-beta.2 2026-09-15 22:51:40 +08:00
Levi 45d7b6f5bc Merge latest main into 2.0.0-beta.2 2026-09-15 22:51:31 +08:00
Levi 5845e9ea5f Merge branch 'main' into main 2026-09-15 22:49:37 +08:00
sun f9410bbcc7 fix(hls): snapshot state in Store and publish fully initialized runs
Address the completion review findings on top of the upstream review
fixes:

- Store runs on the engine's checkpoint goroutine without the task
  lock and serializes the returned value afterwards: take the fetcher
  mutex and return a deep copy instead of the live state pointer,
  matching the HTTP protocol's Store pattern
- Load the journal and per-run base counters before publishing the run
  as active, so Stats/Progress never observe half-initialized fields
- Bind the journal fingerprint to the key IV as well: a rotated IV on
  an otherwise identical plan would silently decrypt to garbage
- Keep every EXT-X-MEDIA member per group instead of only the first
  one, so a group with a URI on a later member is still detected
- After a merge that raced with a pause, keep the staged segments and
  exit silently instead of cleaning them up; drop a dead context
  check and an unused helper, and make the MAP identity and merge
  part-file names unambiguous
2026-09-15 16:31:35 +08:00
sun f1f17948c6 fix(hls): enforce supported HLS subset with explicit errors
The merge pipeline concatenates stored bytes, so features it cannot
reproduce faithfully are now detected and rejected with a clear error
instead of silently succeeding with a broken output:

- EXT-X-GAP (a hole in the merged file) and EXT-X-DISCONTINUITY
  (a broken timeline)
- separate audio renditions referenced by the selected variant: the
  master EXT-X-MEDIA groups are parsed and an AUDIO group with its own
  URI fails the resolve; unreferenced groups are ignored
- changing init sections: a repeated EXT-X-MAP is compared by all of
  its attributes (URI and BYTERANGE), not just the URI
- encrypted EXT-X-MAP without an explicit IV: the spec does not define
  a sequence-derived IV for init sections
- non-identity AES-128 KEYFORMAT values

The supported matrix is documented in the package doc.

Add regression tests for every rejection plus real media validation:
ffmpeg-generated TS segments and fMP4 init/fragments are committed as
fixtures, downloaded through the fetcher (including an AES-128 key
rotation variant), and the merged outputs are validated structurally -
TS on the 188 byte grid with continuity counter rules, PAT/PMT and PES
presence, fMP4 by walking the ISO-BMFF box hierarchy.
2026-09-15 11:42:06 +08:00
sun 675028c64a fix(hls): isolate staging per task and finalize output atomically
- Give every task a random staging identity persisted in the saved
  state: two tasks downloading the same playlist into the same
  directory get separate segment folders and journals instead of
  deleting each other's progress
- Bind the journal to a fingerprint of the full download plan (media
  URL, ordered segments, ranges, keys); foreign or unreadable journals
  are discarded rather than applied to the wrong segments, and the
  ledger is rewritten through a temp file + rename
- Legacy states without a staging identity migrate to a fresh folder
  instead of reusing the shared URL-hash folder
- Merge into a unique temporary file next to the target, check write
  and close errors, then remove-then-rename over the final name: a
  failed merge preserves the staged segments for a retry and re-merging
  after a crash stays idempotent
- Drop the staging folder when a URL patch invalidates the plan
2026-09-15 11:27:26 +08:00
sun b6d18a267b fix(hls): rebuild request extras on restore and adopt merge semantics in Patch
- Parse the request extra in Setup so restored fetchers resend Referer,
  Cookie and other configured headers on resumed segment/key requests;
  the store/restore regression test round-trips the state and the task
  meta through JSON exactly like the engine storage does
- Rebuild the resolved plan on Start when the resource survived but the
  plan was lost to a crash between the task save and the first
  checkpoint, keeping the existing output name
- Patch now merges partial updates instead of replacing the request and
  options wholesale: URL-only patches keep the output path and renamed
  file, header-only patches keep the resolved plan and take effect on
  the next run, Labels merge per key, Proxy swaps rebuild the client,
  and nil arguments are no longer a panic. Editing a running task is
  rejected, consistent with the ed2k protocol
- Cover retry-after-failure counter resets, running-task patch
  rejection and the missing-state self-heal with regression tests
2026-09-15 11:18:40 +08:00
sun 40cfb167f6 fix(hls): isolate per-run lifecycle and use final-output byte accounting
- Give every Start its own run context, WaitGroup (including the queue
  producer), supervisor done channel and snapshots of the plan, request
  headers, HTTP client and merge target; Pause/Close wait for all run
  goroutines, including a merge in flight, before returning
- Build the HTTP client in Resolve/Start before workers launch instead of
  lazily inside do(), which raced between concurrent segment downloads
- Reject task edits while a run is active instead of mutating shared state
- Reset counters per run and seed them from the completed-segment journal;
  count only stored output bytes as progress, never discarded retry bytes
- Report Res.Size only when every segment is unencrypted with a known
  length; ciphertext sums overstate the final output, indeterminate plans
  rely on the engine's TotalDownloaded backfill at completion
2026-09-15 11:13:38 +08:00
Levi 4fd1939732 fix(ffmpeg): reserve capacity before creating media inputs 2026-09-15 10:08:29 +08:00
Levi 3ed54ba93a fix(macos): resign WebView input focus before disposal 2026-09-15 09:15:27 +08:00
Levi fe64a328ac fix(webview): synchronize Windows browser shutdown before reopening profiles 2026-09-15 09:06:56 +08:00
Levi 7e49b678f2 fix(webview): release retained macOS views before profile removal 2026-09-15 09:01:58 +08:00
Levi 311e002e55 Return persistent cookie expiration through the existing RPC contract 2026-09-15 00:56:50 +08:00
Levi 119fad9a94 Use explicit WebView2 shutdown and verify persistent cookie expiration 2026-09-15 00:54:12 +08:00
Levi e2601ea9fa Keep Linux WebViews on one persistent GTK UI thread 2026-09-15 00:52:05 +08:00
Levi a5eaa6b528 Wait for cookie propagation and identify profile assertion stages 2026-09-15 00:51:07 +08:00
Levi bed624bfce Assert HTTPS proxy routing independently of native error-page behavior 2026-09-15 00:44:20 +08:00
Levi eec53c15b2 Extend existing provider contracts and remove duplicate WebView CI 2026-09-15 00:41:47 +08:00
Levi d58135db9f Wait for Android native views to close before clearing profiles 2026-09-14 10:13:08 +08:00
Levi d642ae5018 Remove extension WebView data during uninstall 2026-09-14 10:11:53 +08:00
Levi e358123b22 fix(flutter): keep waiting task progress static 2026-09-14 10:02:19 +08:00
sun ee9b16794e Merge branch 'GopeedLab:main' into main 2026-09-14 09:55:31 +08:00
sun 4f74101b3a feat(hls): add native HLS (m3u8) download protocol
New hls protocol in internal/protocol/hls (registered ahead of http):
- master playlist variant picking (highest bandwidth), RFC3986 segment
  URL resolution, fMP4 init segments, EXT-X-BYTERANGE, EXT-X-GAP skip
- AES-128 decryption with mid-playlist key rotation and sequence-derived
  IVs; explicit errors for live streams, SAMPLE-AES and multi-period fMP4
- concurrent segment downloads with retry, journal-based resume across
  pause/app restarts, automatic in-order merge into a single .ts/.mp4
- best-effort Content-Length probing for determinate progress
- anti-hotlink Referer/Cookie/UA via task extra headers (Go transport
  does not filter them)

Core: fix SchemeFilter extension match for URLs with query strings
(also benefits .torrent links); Flutter: Protocol.hls enum, hls protocol
config model (prevents config round-trip wipe) and HLS settings block
with localized labels in all 22 locales.

Tests: parser/crypto unit tests, httptest e2e (Referer-enforced CDN,
encrypted streams, byterange, pause/resume, store/restore) and an
opt-in live-site acceptance gate (HLS_LIVE_URL).
2026-09-14 09:55:31 +08:00
Levi c47b735fc4 Preserve existing profile connections when opening more pages 2026-09-14 00:43:50 +08:00
Levi ca98806dcc Pin verified native backend and complete proxy regression checks 2026-09-14 00:34:27 +08:00
Levi a0e84a75b1 Isolate extension WebView profiles and follow host proxy settings 2026-09-14 00:30:52 +08:00
Levi 14b22cf485 test: align failed task speed assertion with PR #1510 behavior 2026-09-13 22:27:32 +08:00
Levi a3f6ec5885 Merge latest PR #1512 into 2.0.0-beta.2 2026-09-13 22:25:20 +08:00
Levi f839c3ac93 Merge latest PR #1511 into 2.0.0-beta.2 2026-09-13 22:25:20 +08:00
Levi 67d4aa9554 Merge latest PR #1510 into 2.0.0-beta.2
# Conflicts:
#	ui/flutter/lib/features/extensions/presentation/pages/extensions_page.dart
#	ui/flutter/test/widget_test.dart
2026-09-13 22:25:20 +08:00
Levi c355f9ff5c fix: retain zero download speed for known-size tasks 2026-09-13 18:56:37 +08:00
Levi 59a7d40bfe fix: select downloading tab after mobile task creation 2026-09-13 18:46:19 +08:00
Levi 5d83e91ce5 fix(stream): bound FFmpeg memory buffers and propagate Blob backpressure 2026-09-13 18:24:32 +08:00
Levi 637004c8a8 fix(ffmpeg): use app-writable temporary directory for stream buffers 2026-09-13 18:00:10 +08:00
Levi e79b37d08c fix: remove mobile reveal-to-open-file fallback 2026-09-13 17:39:54 +08:00
Levi d393d27ec7 fix: hide completed task folder action on mobile and web 2026-09-13 17:38:34 +08:00
Levi 2e8672273c feat: confirm extension updates with progress and retry 2026-09-13 17:25:31 +08:00
Levi 4802802828 fix: limit URL install loading to its submit button 2026-09-13 17:20:08 +08:00
Levi 3d372f98de fix: preserve install URL paste focus and limit local installs to desktop 2026-09-13 17:18:13 +08:00
Levi d578fe96bc test(ui): cover minimum extension card width 2026-09-13 15:39:04 +08:00
Levi 1262844363 fix(ui): keep three extension columns at default width 2026-09-13 15:36:36 +08:00
Levi 91bea83692 Merge PR #1512 into 2.0.0-beta.2 2026-09-13 15:30:10 +08:00
Levi e21bc5eeae Merge PR #1511 into 2.0.0-beta.2 2026-09-13 15:30:10 +08:00
Levi 6331060171 Merge PR #1510 into 2.0.0-beta.2 2026-09-13 15:30:10 +08:00
Levi f3ec6b8ee9 Merge latest main into 2.0.0-beta.2 2026-09-13 15:30:00 +08:00
Levi 9124976b5c feat(extension): add streaming FFmpeg WASM merge runtime 2026-09-13 15:23:29 +08:00
Levi 9820e58de9 Merge branch 'main' of github.com:GopeedLab/gopeed into codex/fix-task-detail-text-selection 2026-09-13 15:07:02 +08:00
Levi e890e593d9 feat(ui): compact number display in extension stats 2026-09-13 15:06:42 +08:00
Levi 6572e9e899 fix: automatically submit scheme extension installs 2026-09-13 15:01:50 +08:00
Levi 1c57b07575 fix: prefill scheme extension installs and forward iOS deep links 2026-09-13 14:31:36 +08:00
Levi 6f12eb1f07 Merge Android cold-start back fix and bottom exit prompt into beta 2026-09-12 20:40:25 +08:00
Levi bd03d3b31c fix(android): intercept back during cold startup and move exit toast down 2026-09-12 20:40:07 +08:00
Levi d330d8bea8 Merge mobile back navigation and exit confirmation into beta 2026-09-12 19:03:24 +08:00
Levi ee6b073bc1 fix(ui): preserve mobile home route and confirm Android exit 2026-09-12 18:56:52 +08:00
Levi a41c6c145c Merge latest PR #1505: compact creation button and inline empty-list guidance 2026-09-12 17:49:31 +08:00
Levi 86c8383584 feat(ui): simplify empty-list guidance with inline create action 2026-09-12 17:48:14 +08:00
Levi 75462fe2be fix(ui): make mobile task creation button compact 2026-09-12 17:28:42 +08:00
Levi f35111e072 fix(ui): make mobile task creation button compact 2026-09-12 17:27:44 +08:00
Levi 6aabe69f15 test(ui): align window title expectations with main 2026-09-12 16:56:06 +08:00
Levi 527539f3a1 Merge PR #1506: fix history search text alignment and clipping 2026-09-12 16:55:20 +08:00
Levi 2d239495a0 Merge PR #1505: emphasize task creation and empty-list actions 2026-09-12 16:55:20 +08:00
Levi ddce649c8b Merge latest main into 2.0.0-beta.2 2026-09-12 16:55:20 +08:00
Levi 29675858f2 fix(ui): align history search placeholder and prevent clipping 2026-09-12 10:03:08 +08:00
Levi 1fc0ff6738 feat: emphasize task creation and add empty-list actions 2026-09-12 09:57:02 +08:00
Levi 0851e27c6d Merge PR #1496: fix script settings path picker 2026-09-10 23:10:58 +08:00
Levi 3abc8d42fa Merge PR #1498: fix macOS download notifications 2026-09-10 23:10:53 +08:00
Levi 71d7d96cc0 Merge PR #1499: fix Windows notification icon
# Conflicts:
#	ui/flutter/test/features/tasks/presentation/create_task_window_page_test.dart
2026-09-10 23:10:49 +08:00
Levi f254a82d9c refactor(windows): use bundled notification icon path directly 2026-09-10 23:07:14 +08:00
Levi c4ae3330b8 fix(windows): persist PNG icon for download notifications 2026-09-10 23:00:24 +08:00
Levi a45b6455be fix(macos): initialize file picker for non-sandboxed windows 2026-09-10 14:04:43 +08:00
Levi 979988a7f4 fix(macos): request permission for download notifications 2026-09-10 13:03:51 +08:00
Levi 9efe7ff4c9 Merge PR #1488 latest head: create task panel mobile UI 2026-09-10 00:26:07 +08:00
Levi 77734c69bc Merge PR #1494 latest head: unify platform window titles 2026-09-10 00:26:03 +08:00
Levi 22db46c68a Merge PR #1495 latest head: infer extension for opaque URLs 2026-09-10 00:26:00 +08:00
Levi 99a62357ab Merge PR #1496 latest head: reuse shared file path picker 2026-09-10 00:25:56 +08:00
Levi 9558042513 Merge PR #1497 latest head: fix(android) restore notification permission prompt 2026-09-10 00:25:50 +08:00
Levi 39912718c4 fix(android): request notification permission on startup 2026-09-10 00:19:53 +08:00
Levi 3b6b134f24 ci: warn on incomplete localization catalogs 2026-09-10 00:02:31 +08:00
Levi f0aa44455c Merge branch 'main' into codex/fix-settings-script-path-picker 2026-09-09 23:41:17 +08:00
Levi 99ed770017 fix(ui): reuse shared file path picker for script settings 2026-09-09 23:39:44 +08:00
Levi f506b8f92f Merge branch 'main' into fix/create-task-panel-mobile-ui 2026-09-09 23:30:41 +08:00
Levi 9b9fd12884 Merge branch 'main' into fix/create-task-panel-mobile-ui 2026-09-09 23:27:11 +08:00
dajiaohuang cea912b414 fix(http): preserve MIME extension for opaque URLs 2026-09-09 03:45:15 +08:00
Levi 7c3d7269fe fix: unify platform window titles as Gopeed 2026-09-08 23:15:55 +08:00
Levi 1554791e17 merge: PR #1493 refresh repeated Android download intents
# Conflicts:
#	ui/flutter/test/features/tasks/presentation/create_task_window_page_test.dart
2026-09-08 22:14:48 +08:00
Levi b0aecdcfc6 merge: latest PR #1488 mobile resolve action layout 2026-09-08 22:14:14 +08:00
Levi d2eb7b2a67 fix(android): refresh download URL when create page is already open 2026-09-08 22:05:45 +08:00
Levi 7112845c82 fix: keep resolve dialog actions equal in width 2026-09-08 21:53:25 +08:00
Levi 39820707be merge: PR #1488 extension toolbar alignment fix 2026-09-08 21:28:33 +08:00
Levi 47db539d91 fix: align extension search with compact toolbar controls 2026-09-08 21:27:52 +08:00
Levi f32f7583e4 fix: align extension search with compact toolbar controls 2026-09-08 21:25:05 +08:00
Levi e3ae2865d6 fix: reconcile Azerbaijani locale with merged create action 2026-09-08 20:26:38 +08:00
Levi 1a8d73fbe6 merge: PR #1489 Android browser takeover handling 2026-09-08 20:24:00 +08:00
Levi 5728caf7db merge: PR #1488 create task mobile UI 2026-09-08 20:23:52 +08:00
Levi 77d95971ca merge: PR #1476 async native API callbacks 2026-09-08 20:23:52 +08:00
Levi 2c81520029 merge: minimal macOS window reopen fix 2026-09-08 20:23:52 +08:00
Levi 8d45eb4fef test: remove native window regression tests 2026-09-08 20:03:57 +08:00
Levi 31fa0e5bc9 fix: restore only main macOS window and select active downloads 2026-09-08 18:14:16 +08:00
Levi 0f60f40e3e Merge branch 'main' into codex/async-ffi-worker-callbacks 2026-09-07 09:23:59 +08:00
Levi b17c0fd7c8 fix(android): preserve browser takeover handling 2026-09-07 09:11:17 +08:00
Levi a834ca0d74 fix(ui): unify text field appearance 2026-09-06 22:26:35 +08:00
Levi ecfd07ccb0 fix(ui): refine create task panel behavior 2026-09-06 22:17:26 +08:00
Levi d51eeb3f2c fix: prevent native requests from blocking shutdown 2026-09-06 21:49:11 +08:00
Levi 1bb3f74c9f Merge branch 'main' into codex/async-ffi-worker-callbacks 2026-09-06 20:37:14 +08:00
Levi f7be97667f Merge remote-tracking branch 'origin/main' into codex/async-ffi-worker-callbacks 2026-09-06 13:00:59 +08:00
Levi 607d1729e6 refactor: share async invoke bridge across native platforms 2026-09-06 11:59:11 +08:00
Levi d12122ea86 feat: make desktop FFI invocation asynchronous 2026-09-06 11:23:27 +08:00
349 changed files with 3338 additions and 38897 deletions

No files matched your search

+6 -21
View File
@@ -13,7 +13,7 @@ on:
default: "false" default: "false"
env: env:
GO_VERSION: "1.26.11" GO_VERSION: "1.25.4"
FLUTTER_VERSION: "3.41.6" FLUTTER_VERSION: "3.41.6"
GA4_MEASUREMENT_ID: ${{ secrets.GA4_MEASUREMENT_ID }} GA4_MEASUREMENT_ID: ${{ secrets.GA4_MEASUREMENT_ID }}
GA4_API_SECRET: ${{ secrets.GA4_API_SECRET }} GA4_API_SECRET: ${{ secrets.GA4_API_SECRET }}
@@ -106,32 +106,17 @@ jobs:
if ("${{ matrix.arch }}" -eq "amd64") { if ("${{ matrix.arch }}" -eq "amd64") {
# for amd64 # for amd64
$release = "build\windows\x64\runner\Release\" $release = "build\windows\x64\runner\Release\"
# Use the same toolchain as cgo instead of Git's bundled DLLs. $mingw = "C:\Program Files\Git\mingw64\bin"
$cc = (Get-Command -Name (go env CC) -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source cp $mingw\libstdc++-6.dll $release
foreach ($dll in @("libstdc++-6.dll", "libgcc_s_seh-1.dll", "libwinpthread-1.dll")) { cp $mingw\libgcc_s_seh-1.dll $release
$runtimePath = [string](& $cc "-print-file-name=$dll")
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($runtimePath)) {
throw "Failed to locate $dll using $cc"
}
$runtimePath = $runtimePath.Trim()
# Some MinGW distributions keep runtime DLLs beside gcc.exe.
if ($runtimePath -eq $dll) {
$runtimePath = Join-Path (Split-Path -Parent $cc) $dll
}
if (!(Test-Path -LiteralPath $runtimePath -PathType Leaf)) {
throw "MinGW runtime $dll was not found by $cc (reported path: $runtimePath)"
}
Write-Host "Copying MinGW runtime: $runtimePath"
Copy-Item -LiteralPath $runtimePath -Destination $release
}
} else { } else {
# for ARM64 # for ARM64
$release = "build\windows\arm64\runner\Release\" $release = "build\windows\arm64\runner\Release\"
$mingw = "$env:CLANGARM64_ROOT\aarch64-w64-mingw32\bin" $mingw = "$env:CLANGARM64_ROOT\aarch64-w64-mingw32\bin"
cp $mingw\libc++.dll $release cp $mingw\libc++.dll $release
cp $mingw\libunwind.dll $release cp $mingw\libunwind.dll $release
cp $mingw\libwinpthread-1.dll $release
} }
cp $mingw\libwinpthread-1.dll $release
cp $system\msvcp140.dll $release cp $system\msvcp140.dll $release
cp $system\vcruntime140.dll $release cp $system\vcruntime140.dll $release
cp $system\vcruntime140_1.dll $release cp $system\vcruntime140_1.dll $release
@@ -798,5 +783,5 @@ jobs:
linux/arm64 linux/arm64
linux/arm/v7 linux/arm/v7
tags: | tags: |
${{ !contains(env.VERSION, 'beta') && 'liwei2633/gopeed:latest' || '' }} liwei2633/gopeed:latest
liwei2633/gopeed:v${{ env.VERSION }} liwei2633/gopeed:v${{ env.VERSION }}
+1 -1
View File
@@ -17,7 +17,7 @@ on:
- "Dockerfile" - "Dockerfile"
env: env:
GO_VERSION: "1.26.11" GO_VERSION: "1.25.4"
permissions: permissions:
contents: write contents: write
+1 -47
View File
@@ -28,7 +28,7 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
GO_VERSION: "1.26.11" GO_VERSION: "1.25.4"
FLUTTER_VERSION: "3.41.6" FLUTTER_VERSION: "3.41.6"
jobs: jobs:
@@ -69,49 +69,6 @@ jobs:
with: with:
files: ./coverage.txt files: ./coverage.txt
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
test-flutter-category:
# The settings editor and the backend have to read an extension list the
# same way, so both sides run the shared rule table. The Go side is covered
# by the test job above, this runs the ui side of it.
name: Test shared category rules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Run the shared category rule table on the ui side
working-directory: ui/flutter
run: |
flutter pub get
flutter gen-l10n
flutter test test/api/downloader_config_test.dart
test-flutter-task-context-menu:
name: Test task context menus and text inputs
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CHROME_EXECUTABLE: /usr/bin/google-chrome
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Prepare Flutter tests
working-directory: ui/flutter
run: |
flutter pub get
flutter gen-l10n
- name: Test task menu actions and native input policy
working-directory: ui/flutter
run: |
flutter test --no-pub test/features/tasks/presentation/widgets/task_context_menu_test.dart test/shared/widgets/app_text_field_test.dart
flutter test --no-pub test/widget_test.dart --plain-name 'task context menu exposes'
- name: Test browser menu cancellation and input policy
working-directory: ui/flutter
run: |
"$CHROME_EXECUTABLE" --version
flutter test --no-pub --platform chrome test/features/tasks/presentation/widgets/task_context_menu_test.dart test/shared/widgets/app_text_field_test.dart
test-webview-integration: test-webview-integration:
name: Test webview integration (${{ matrix.name }}) name: Test webview integration (${{ matrix.name }})
strategy: strategy:
@@ -134,9 +91,6 @@ jobs:
if: runner.os == 'macOS' if: runner.os == 'macOS'
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Test Windows FFmpeg memory and stream lifecycle
if: runner.os == 'Windows'
run: go test -v -count=1 -timeout 180s ./pkg/download/engine/ffmpeg ./pkg/download/engine/inject/stream ./pkg/download/engine
- name: Install Linux WebView dependencies - name: Install Linux WebView dependencies
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: | run: |
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.26.11-alpine3.23 AS go FROM golang:1.25.4-alpine3.22 AS go
WORKDIR /app WORKDIR /app
COPY ./go.mod ./go.sum ./ COPY ./go.mod ./go.sum ./
RUN go mod download RUN go mod download
+1 -1
View File
@@ -20,7 +20,7 @@ Beyond core download management, Gopeed offers browser integration, JavaScript e
Visit ✈ [Official Website](https://gopeed.com) Visit ✈ [Official Website](https://gopeed.com)
![Application screenshot](_docs/img/ui-concept-en.webp) ![Application screenshot](_docs/img/ui-concept-en.png)
## ✨ Features ## ✨ Features
+1 -1
View File
@@ -20,7 +20,7 @@ Gopeed(**Go Speed** の略)は、Go と Flutter で開発された高速で
✈ [公式サイト](https://gopeed.com) ✈ [公式サイト](https://gopeed.com)
![アプリのスクリーンショット](_docs/img/ui-concept-en.webp) ![アプリのスクリーンショット](_docs/img/ui-concept-en.png)
## ✨ 主な機能 ## ✨ 主な機能
+1 -1
View File
@@ -20,7 +20,7 @@ Ngoài việc quản lý tải xuống hằng ngày, Gopeed còn hỗ trợ tíc
Truy cập ✈ [Trang web chính thức](https://gopeed.com) Truy cập ✈ [Trang web chính thức](https://gopeed.com)
![Ảnh chụp màn hình ứng dụng](_docs/img/ui-concept-en.webp) ![Ảnh chụp màn hình ứng dụng](_docs/img/ui-concept-en.png)
## ✨ Tính năng chính ## ✨ Tính năng chính
+1 -1
View File
@@ -20,7 +20,7 @@ Gopeed(全称 **Go Speed**,也叫“够快下载器”)是一款使用 Go
访问 ✈ [官方网站](https://gopeed.com) 访问 ✈ [官方网站](https://gopeed.com)
![应用截图](_docs/img/ui-concept-zh-CN.webp) ![应用截图](_docs/img/ui-concept-zh-CN.png)
## ✨ 主要特性 ## ✨ 主要特性
+1 -1
View File
@@ -20,7 +20,7 @@ Gopeed(**Go Speed** 的縮寫)是一款以 Go 與 Flutter 開發的高速、
瀏覽 ✈ [官方網站](https://gopeed.com) 瀏覽 ✈ [官方網站](https://gopeed.com)
![應用程式截圖](_docs/img/ui-concept-en.webp) ![應用程式截圖](_docs/img/ui-concept-en.png)
## ✨ 主要功能 ## ✨ 主要功能
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 792 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 787 KiB

-52
View File
@@ -1,52 +0,0 @@
# Batch task creation
`POST /api/v1/tasks/batch` accepts ordinary and grouped batches:
```json
{
"group": true,
"groupName": "album",
"opts": {"path": "/downloads"},
"reqs": [
{"rid": "resolved-resource-id", "opts": {"selectFiles": [1]}},
{"req": {"url": "https://example.com/cover.jpg"}}
]
}
```
Each member accepts `rid` or `req`. A nonempty `rid` takes priority and creates a
task from the cached resolve result. A member's `opts`, when present, replaces
batch `opts` as a whole; omitted fields do not inherit individual batch values.
Members without `opts` use batch `opts`. BT file selections use the indexes from
the resolve result. Requests without `rid` continue to work.
For groups, batch `opts.path` selects the parent directory. Members inheriting
batch options, or with an empty path, use the reserved group directory. Explicit
relative member paths are resolved under that directory; do not prefix them with
the batch parent path. For example, parent `downloads` and member path `nested`
place the member in `downloads/<group name>/nested`. Absolute paths under the
parent directory preserve their relative structure within the group. In ordinary
batches, an empty member path uses the configured default download directory.
All members are attempted. If at least one task is persisted, the response is
successful and contains the created task IDs. A task that fails to start after
being persisted is included; use the existing task controls to retry it.
Successful tasks are kept when other members fail. If every member fails, the
API returns an error and removes any newly reserved empty group and directory.
Both ordinary and grouped batches now return an object in `data`:
```json
{"code": 0, "msg": "", "data": {"taskIds": ["task-1", "task-2"], "parentId": "group-id"}}
```
Ordinary batches omit `parentId`. **This replaces the former array response for
ordinary batches.** The outer `code`/`msg`/`data` envelope is unchanged. Clients
must read `data.taskIds`; there are no per-member results. A successful response
completes the batch, including partial success, so clients must not automatically
resubmit missing members.
With `group: true`, the server generates the group ID and reserves its directory.
Creation does not accept a client group ID or append to an existing group. Group
metadata remains available through the existing task list and detail responses;
`parentId` identifies membership for group operations.
+1 -7
View File
@@ -1,9 +1,6 @@
package main package main
import ( import (
"fmt"
"os"
"github.com/GopeedLab/gopeed/cmd" "github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
) )
@@ -16,8 +13,5 @@ func main() {
Storage: model.StorageBolt, Storage: model.StorageBolt,
WebEnable: true, WebEnable: true,
} }
if err := cmd.Start(cfg); err != nil { cmd.Start(cfg)
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
} }
+2 -2
View File
@@ -166,7 +166,7 @@ func runResolve(args []string) error {
result, err := d.Resolve(&base.Request{ result, err := d.Resolve(&base.Request{
URL: url, URL: url,
Labels: map[string]string(labels), Labels: map[string]string(labels),
}) }, nil)
if err != nil { if err != nil {
return err return err
} }
@@ -250,7 +250,7 @@ func runDownload(args []string) error {
result, err := d.Resolve(&base.Request{ result, err := d.Resolve(&base.Request{
URL: url, URL: url,
Labels: map[string]string(labels), Labels: map[string]string(labels),
}) }, nil)
if err != nil { if err != nil {
return err return err
} }
+6 -14
View File
@@ -16,18 +16,17 @@ import (
//go:embed banner.txt //go:embed banner.txt
var banner string var banner string
func Start(cfg *model.StartConfig) error { func Start(cfg *model.StartConfig) {
fmt.Println(banner)
srv, listener, err := rest.BuildServer(cfg) srv, listener, err := rest.BuildServer(cfg)
if err != nil { if err != nil {
return err panic(err)
} }
fmt.Println(banner)
downloadCfg, err := rest.Downloader.GetConfig() downloadCfg, err := rest.Downloader.GetConfig()
if err != nil { if err != nil {
listener.Close() panic(err)
return err
} }
if rest.Downloader.IsFirstLoad() { if downloadCfg.FirstLoad {
// Set default download config // Set default download config
if cfg.DownloadConfig != nil { if cfg.DownloadConfig != nil {
cfg.DownloadConfig.Merge(downloadCfg) cfg.DownloadConfig.Merge(downloadCfg)
@@ -52,20 +51,13 @@ func Start(cfg *model.StartConfig) error {
rest.Downloader.PutConfig(downloadCfg) rest.Downloader.PutConfig(downloadCfg)
} }
} }
// The default download dir is resolved only after the core starts, so
// seed the built-in categories now that the dir is known. Init is
// idempotent and leaves an existing category list untouched.
downloadCfg.Init()
rest.Downloader.PutConfig(downloadCfg)
} }
watchExit() watchExit()
fmt.Printf("Server start success on http://%s\n", listener.Addr().String()) fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
return err panic(err)
} }
return nil
} }
func watchExit() { func watchExit() {
-35
View File
@@ -1,35 +0,0 @@
package cmd
import (
"strings"
"testing"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
func TestStartInvalidWebAPITokenReturnsError(t *testing.T) {
tests := []struct {
name string
webAuth *model.WebAuth
}{
{name: "no credentials"},
{name: "missing username", webAuth: &model.WebAuth{Password: "secret"}},
{name: "missing password", webAuth: &model.WebAuth{Username: "gopeed"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Start(&model.StartConfig{
WebEnable: true,
ApiToken: "test-token",
WebAuth: tt.webAuth,
})
if err == nil {
t.Fatal("Start() error = nil, want web authentication validation error")
}
if !strings.Contains(err.Error(), "web authentication username and password are required") {
t.Fatalf("Start() error = %q, want missing web authentication credentials", err)
}
})
}
}
+2 -2
View File
@@ -52,8 +52,8 @@ func loadCliArgs() *args {
flag.StringVar(cfg.Username, "username", "gopeed", "Web Authentication Username") flag.StringVar(cfg.Username, "username", "gopeed", "Web Authentication Username")
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled") cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
flag.StringVar(cfg.Password, "password", "", "Web Authentication Password, if no password is set, web authentication will not be enabled") flag.StringVar(cfg.Password, "password", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
cfg.ApiToken = flag.String("T", "", "API token (requires web authentication with username and password)") cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
flag.StringVar(cfg.ApiToken, "api-token", "", "API token (requires web authentication with username and password)") flag.StringVar(cfg.ApiToken, "api-token", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
flag.BoolVar(&cfg.MCPEnable, "mcp-enable", false, "Enable MCP endpoint") flag.BoolVar(&cfg.MCPEnable, "mcp-enable", false, "Enable MCP endpoint")
cfg.TempDir = flag.String("temp-dir", "", "Temporary directory (defaults to the OS temporary directory)") cfg.TempDir = flag.String("temp-dir", "", "Temporary directory (defaults to the OS temporary directory)")
cfg.StorageDir = flag.String("d", "", "Storage directory") cfg.StorageDir = flag.String("d", "", "Storage directory")
+1 -4
View File
@@ -62,10 +62,7 @@ func main() {
WebFS: sub, WebFS: sub,
WebAuth: webAuth, WebAuth: webAuth,
} }
if err := cmd.Start(cfg); err != nil { cmd.Start(cfg)
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
} }
func isNotBlank(str *string) bool { func isNotBlank(str *string) bool {
-3
View File
@@ -1,3 +0,0 @@
/testdata/extensions/
/bin/
__pycache__/
-40
View File
@@ -1,40 +0,0 @@
# moejs 实验评估
目标按顺序判断:**先能跑通 Gopeed 的真实扩展,再判断收益是否足够大,最后才考虑正式替换。**
这是独立 Go module。生产引擎、根目录依赖和默认构建不变。结论、实测数据及限制见 [REPORT.md](REPORT.md)。
## 复现
从本目录运行,需要 Git、Python 3 和 Go 1.26.0:
```sh
python3 fetch_sources.py
go test -v -timeout 3m
# 可选:向 Bilibili / YouTube / Twitter 等公开接口发请求;不需要账户 Cookie。
GOPEED_MOEJS_LIVE=1 go test -run TestLiveResolve -v -timeout 4m
# 单独执行,期间避免其他构建/测试负载。
GOMAXPROCS=1 go test -pgo=off -run '^$' -bench . -benchmem -benchtime=500ms -count=5 -timeout 8m > results/bench.txt
python3 summarize.py
python3 measure_size.py
# macOS/Linux:独立进程的 CPU、峰值 RSS、存活堆和关闭后内存。默认 5 轮。
python3 measure_resources.py > results/resources-run.txt
```
`fetch_sources.py` 只取固定提交的原始 `dist/index.js`,校验已记录的 SHA-256,不执行 npm 安装或构建。下载文件和二进制不入库。`results/sources.json` 是来源清单。
## 实验范围
- goja 基线调用真实的 `pkg/download/engine.NewEngine`。
- moejs 原型使用其 eventloop、Web API,加载 Gopeed 现有的 polyfill 和 XHR JS;保留 mutable builtins,与 goja 的默认语义接近。
- 三个完整扩展包执行 `onResolve`,两端逐字比较资源输出和请求路径;B 站包含 SDK/WBI 签名逻辑,YouTube 包含 YouTube.js 的元数据解析。
- `replay.js` 用固定的合成 HTTP 响应隔离网络波动;没有把 SDK 函数替换成假函数。YouTube 的 session/config 请求固定返回 503,走库的本地 session fallback,再提供 player JSON。
- Blob 注册、FFmpeg 和 WebView 为模拟宿主。FFmpeg 的 mock 会拒绝真正合并;WebView 只接受 UA 查询。`onStart/onError` 只验证 mock 宿主下的回调流程,**不代表下载或 SABR/PoToken 已验证**。
- `TestMoeResolveWriteback` 演示显式的 Go request 拷入、JS resource 拷出,并验证 Gopeed `Resource.Validate()`。真实任务的可变对象/方法还需专门移植。
- `TestAsyncHTTP` 使用本地 HTTP server,验证异步 JSON 和 AbortController 请求取消。
- `BenchmarkBufferedTransport` 在两个引擎中使用同一段 JS 的 buffered Response,检查速度差是否主要来自原生 Response/Stream 实现。
- `BenchmarkExtension/*/cold` 包含建引擎、polyfill、编译和加载完整 bundle、一次 resolve 及关闭;`warm` 复用已加载的 runtime。冷启动更接近当前 `doTrigger` 路径。
- `measure_resources.py` 为每个样本启动独立 worker,交替引擎顺序,以进程 user+system CPU 时间比较相同工作量;另测保留 1/8 个 runtime 的内存。解释与结果见 [RESOURCES.md](RESOURCES.md)。两个引擎都链接在同一个测量程序中,RSS 是原型 worker 的占用,不是最终 Gopeed 应用占用。
- `measure_size.py` 实测独立解释器程序和真实 CLI 的双引擎共存成本;没有伪造“已经完全替换”的安装包大小。CLI 构建可能临时补充本实验 module 的依赖,最后执行 tidy。
测试只覆盖本报告所列路径。未指定 `GOPEED_MOEJS_LIVE=1` 时,扩展回放不访问外网;固定 bundle 需要事先下载。
-128
View File
@@ -1,128 +0,0 @@
# moejs 替换 goja 的实验评估
日期:2026-11-14。目标顺序:**真实扩展先跑通 → 判断实际收益 → 再决定是否正式替换**。
## 判断
**CPU/内存补测后,建议维持 goja,暂不投入完整替换。**
新增的 [CPU 与内存测量](RESOURCES.md) 表明:当前冷路径 B 站/YouTube 的实际 CPU 时间只降低约 12% / 15%,驻留内存没有一致改善,连续解析的进程峰值 RSS 还更高。结合迁移成本和包体结果,收益不足以推进替换。
三个真实扩展的完整 bundle 已在 moejs 中跑通离线解析和公开网站联网解析,结果与 goja 相同。热运行 CPU 收益明显;当前 Gopeed 的事件路径每次新建引擎、编译并加载 bundle,冷启动的收益远小于热运行倍数。压缩体积基本没有显著收益。
“扩展解析可运行”已验证。“真实下载、FFmpeg 合并、YouTube SABR/PoToken、WebView 和 Go 任务生命周期完整可用”尚未验证,不能把本实验当成这些路径已经兼容。
## 版本与方法
- Gopeed 基线:`be0a395447d788419e83acdfd693e48a1a1706bf`,分支基于该版 main。
- goja:`v0.0.0-20260106131823-651366fbe6e3`,沿用项目依赖。
- moejs:`v0.0.0-20261113160646-b2c7fed9c42d`,固定上游提交;纯 Go,当前主模块的 Go 1.26.0 足够。
- Apple M1、8 GiB RAM、macOS 14.6.1、Go 1.26.0;性能测试 `GOMAXPROCS=1`、`-pgo=off`、每项 500 ms、5 次重复,报告中位数。没有套用上游的 PGO 或上游插件跑分。
- 最终性能测量单独执行,未与本次构建或联网测试并发。记录在 [toolchain.txt](results/toolchain.txt)、[bench.txt](results/bench.txt)。`ns_min/ns_max` 见 [bench-summary.json](results/bench-summary.json)。这不是多机器统计结论。
- goja 使用生产 `engine.NewEngine`;moejs 使用其 eventloop/webapi 加 Gopeed 的 polyfill/XHR。两侧都运行未经改写的发布 bundle。moejs 使用 `MutableIntrinsics: true`,保留 goja 的可写 builtin 语义。
- 冷运行包含建引擎、宿主 JS 初始化、编译/加载 bundle、一次解析及关闭;热运行复用已加载 runtime。宿主初始化能力不完全对等:goja 含完整生产注入,moejs 尚未移植 File/VM/FFmpeg 等原生桥。**冷运行数字是两个现有原型的对比,不是完成生产移植后的承诺。**
- 回放提供合成 HTTP 响应,不替换 Bilibili SDK、YouTube.js 或 Twitter SDK。YouTube session/config 固定返回 503,验证本地 session fallback 与 player JSON 解析;未覆盖成功 config、真实 player decipher 或媒体流。
- 基准中 Gopeed namespace/task/Blob 等由相同 JS mock 提供,结果以 JSON 字符串导出用于比较;不包含完整 Go 对象桥接及持久化的成本,原型的 Resource 拷出另有功能验证,未纳入时延表。
- 每次结果包含文件名、URL、labels、请求路径,两端比较相同输出。额外使用相同 JS 的 buffered Response 再测热运行,降低原生 Response/Stream 差异的影响。
## 真实扩展验证
| 扩展 | 固定发布包提交 | bundle 大小 | 回放 / 联网解析 | 已验证的范围 |
| --- | --- | ---: | --- | --- |
| Bilibili | [`b91db1e`](https://github.com/monkeyWie/gopeed-extension-bilibili/tree/b91db1ea52e975d8de9823f01a7af5f9ea83792f) | 920,556 B | 两个引擎均成功,输出相同 | 真实 SDK/WBI 请求签名、3 分 P、画质选择、资源组装 |
| YouTube | [`eeb78d1`](https://github.com/monkeyWie/gopeed-extension-youtube/tree/eeb78d168e053e9a2aad1dbaa309fc11f6992f7c) | 3,409,614 B | 两个引擎均成功,输出相同 | 完整 YouTube.js bundle 初始化、公开视频 metadata/title、下载任务描述 |
| Twitter | [`a963428`](https://github.com/monkeyWie/gopeed-extension-twitter/tree/a963428bbfdbee9510bf224b00b384e37514ce77) | 66,243 B | 两个引擎均成功,输出相同 | guest token/GraphQL SDK 路径、图片和最高码率视频筛选 |
联网输入:B 站 `BV1BJ4m1e7g8?p=1-3`、YouTube `dQw4w9WgXcQ`、Twitter `1734145541599383880`。未使用账户 Cookie。B 站实际返回了 480p fallback,两个引擎一致。联网不是固定延迟源,不用单次耗时推导速度倍数。
`onStart/onError` 在同一个实验 runtime 的模拟 task/blob/webview 宿主下跑通;B 站 CDN 切换和 YouTube 重试注册能返回相同结果。**这不包含真正调用 Blob opener 或读取/合并媒体。** WebView 只模拟 `navigator.userAgent`,FFmpeg mock 遇到真实 merge 会报错,不会伪造下载成功。
另有两个直接宿主检查:moejs 的显式 Go request 拷入、resource 拷出经过 `base.Resource.Validate()`;本地 HTTP server 的异步 JSON 和取消请求在两侧均可完成。取消后 HTTP server 的请求 context 均结束。但原生产 goja fetch 的取消 catch 路径因 `DOMException` 未定义而返回 `ReferenceError`,moejs 返回传入的取消原因;这个已有宿主差异被单独记录,不能视为错误语义已完全兼容,详见 [http.txt](results/http.txt)。
原始证据:[compatibility.txt](results/compatibility.txt)、[live.txt](results/live.txt)、[sources.json](results/sources.json)。
## 性能实测
### 热解析与冷解析
| 扩展 | 热 goja / moejs (ms) | 热速度倍数 | 冷 goja / moejs (ms) | 冷耗时下降 |
| --- | ---: | ---: | ---: | ---: |
| bilibili | 21.188 / 3.389 | 6.25× | 118.861 / 104.827 | 11.8% |
| youtube | 3.193 / 0.277 | 11.54× | 283.412 / 239.646 | 15.4% |
| twitter | 3.575 / 0.505 | 7.08× | 17.239 / 11.849 | 31.3% |
### 相同 JS buffered Response 的对照
| 扩展 | goja (ms) | moejs (ms) | 速度倍数 | 每次分配 goja / moejs (KiB) |
| --- | ---: | ---: | ---: | ---: |
| bilibili | 22.076 | 3.528 | 6.26× | 8036.0 / 1267.2 |
| youtube | 2.895 | 0.290 | 10.00× | 1071.1 / 151.3 |
| twitter | 3.450 | 0.491 | 7.03× | 1527.4 / 238.3 |
更换为相同的 JS Response 后仍有 6–10 倍差距,说明收益并不只来自 moejs 的原生 Response/Stream。但 URL/Headers 等底层实现仍不同,因此它仍是完整运行时对照,不能宣称是严格隔离的解释器指令效率。
### 空 runtime 加一次 `1+1` 的对照
| 配置 | 耗时 (µs) | 每次分配 (KiB) |
| --- | ---: | ---: |
| goja | 2.85 | 5.1 |
| moejs-mutable | 48.42 | 103.6 |
| moejs-shared | 3.86 | 5.2 |
moejs 的 mutable runtime 创建在本机反而明显更贵。shared frozen 模式能降低这部分成本,但改变 builtin 的可变性;本报告没有把这项不同合同的优化算入默认迁移收益。上述项也包含编译小脚本,并非只测构造函数。
热运行的明显收益不能直接解释成“下载快了数倍”。本次固定 JSON 较小,没有大 playlist、真实 YouTube player JS、PoToken、SABR、账号登录或 FFmpeg 媒体吞吐的测量。`B/op` 是每次调用累计分配量,**不是常驻内存、峰值 RSS 或每个 runtime 的存活内存**。
当前 [`doTrigger`](../../pkg/download/extension.go) 在匹配每个事件后调用 `newExtensionEngine` 并执行 bundle;[`engineSession`](../../pkg/download/extension_engine_session.go) 为 Blob/WebView 保活,未提供已编译 bundle 的通用缓存。因此首个事件更接近 cold 项。即使热运行快很多,也不能套用同样倍数预测首次解析。未来可以单独比较“两引擎均缓存编译产物”,避免把缓存改进归因于换引擎。
## 构建体积
`CGO_ENABLED=0`、`-trimpath -pgo=off -ldflags='-s -w'`。gzip 使用 Python 默认 level 9、`mtime=0`。gzip 只是相同压缩算法的代理,不是 Flutter installer、APK、IPA 或最终 zip 的测量。
### 独立解释器程序
| 目标 | goja (MiB) | moejs (MiB) | 未压缩减少 (MiB) | gzip 减少 (KiB) |
| --- | ---: | ---: | ---: | ---: |
| darwin/arm64 | 8.86 | 7.74 | 1.12 | 36.0 |
| linux/amd64 | 9.14 | 8.06 | 1.07 | 13.0 |
| windows/amd64 | 9.36 | 8.30 | 1.07 | 9.9 |
| android/arm64 | 9.56 | 8.44 | 1.12 | 37.3 |
### 真实 Gopeed CLI(`nosqlite`)的双引擎过渡成本
| 目标 | 原生产 CLI (MiB) | 加入 moejs/webapi (MiB) | 未压缩增加 (MiB) | gzip 增加 (MiB) |
| --- | ---: | ---: | ---: | ---: |
| darwin/arm64 | 49.63 | 56.06 | 6.43 | 2.50 |
| linux/amd64 | 51.25 | 57.92 | 6.68 | 2.69 |
| windows/amd64 | 52.42 | 59.23 | 6.81 | 2.73 |
独立解释器程序支持从命令行传入任意 JS,避免 linker 把执行功能全部裁掉;它不含完整宿主能力。真实 CLI 的第二项通过构建 overlay 额外链接 moejs、eventloop、webapi,**仍然保留生产 goja,默认扩展执行仍走 goja**。这测的是双引擎过渡阶段成本,没有伪装成完整替换的成品。
结论:独立引擎未压缩可以少约 1.1 MiB,但压缩后只省十几到几十 KiB;没有证据表明换引擎能让最终安装包大幅变小。双引擎灰度还会增大包。完全移除 goja 后的 CLI、桌面 c-shared、Android/iOS 库和 Flutter 安装包必须等完整桥接完成后再测。本次 Android 只交叉编译独立解释器,不代表 APK/FFI 已验证。
原始证据:[size.json](results/size.json)、[size-build.txt](results/size-build.txt),构建脚本 [measure_size.py](measure_size.py)。
## 迁移阻塞与工作量
| 接入点 | 实测或代码事实 | 正式替换需要完成 |
| --- | --- | --- |
| Go struct / map 语义 | goja 对 `ctx.res`、`ctx.req.url` 的修改写回 Go;moejs `FromGo` 是 JS 快照,Go 值不变;Go 方法也不会自动映射 | 显式对象/方法包装及写回策略,覆盖 req/task/meta/extra/settings/storage,验证引用与持久化语义 |
| 数字 labels | B 站给 labels 写数字;goja 自动转成 `map[string]string`,直接 JSON 拷出会报类型错 | 原型已做 string 规范化;生产要定义完整转换和错误策略 |
| 事件循环和取消 | `goja_nodejs` 与现有 `Post`、Promise/事件超时直接绑定;moejs 有不同的 loop/API | 对齐并发归属、超时、Close、pending Promise、回调错误、后台任务生命周期,迁移现有 cancellation 测试 |
| fetch/XHR/streams | XHR JS 可复用;本次 fetch 使用 moejs webapi;本地取消请求已验证 | 保留代理、默认 UA、headers、Cookie、stream backpressure、body ownership 和 session cleanup;跑现有 Fetch WPT/流测试于新宿主 |
| Blob/FFmpeg/WebView | 生产注入层大量引用 goja `Value`、`Object`、`FunctionCall` | 移植 Blob opener/reader、FFmpeg WASM、WebView 回调与函数源码序列化,验证取消/恢复/重试 |
| File/VM/MessageError/logging | 现有 Go 方法/错误和值导出不是 moejs 的直接替代 API | 建原生函数包装,保留 `MessageError` 分类、日志格式、File 行为与子 VM 隔离 |
| 可写 builtins | 三个回放也能在 frozen 模式运行,但修改 `Array.prototype` 会失败 | 默认保持 mutable;除非扩展能力合同允许冻结,不能把共享 builtin 的启动/内存优势直接作为默认收益 |
上游 [Go values in](https://github.com/MoeForge/moejs/blob/b2c7fed9c42dd1ba8ce5ea844850cba6a1a95fb3/docs/guide.md#go-values-in) 说明快照/方法转换规则;[runtime.go](https://github.com/MoeForge/moejs/blob/b2c7fed9c42dd1ba8ce5ea844850cba6a1a95fb3/runtime.go) 说明共享冻结 builtin 和 goroutine 约束;[README](https://github.com/MoeForge/moejs/blob/b2c7fed9c42dd1ba8ce5ea844850cba6a1a95fb3/README.md#status) 仍将项目标为 alpha、API 可能变化。要固定提交,不能把上游语言测试通过数当成 Gopeed 宿主兼容证明。分发需要随带上游 [THIRD_PARTY_NOTICES](https://github.com/MoeForge/moejs/blob/b2c7fed9c42dd1ba8ce5ea844850cba6a1a95fb3/THIRD_PARTY_NOTICES)。
## 后续替换门槛
1. 在实验构建中跑完整 Downloader → extension → Go task/resource → Blob/FFmpeg/WebView 路径,实测 B 站与 YouTube 下载输出文件、取消、断点恢复、重试;不再依赖本次模拟宿主。
2. 新引擎跑现有扩展、Fetch/Stream、WebView、FFmpeg、事件取消测试;验证无 pending promise、session 泄漏及并发访问错误。
3. 在实际每事件新建 runtime 的方式下比较总耗时、CPU、峰值/常驻内存和最终安装包;增加至少一台 Windows、一台 Android/iOS 目标以及真实大型 player/playlist。
4. 再决定冷路径与实际解析体验的提升是否足以抵消桥接成本。新增资源测量不支持在当前架构下继续完整移植;未来若有真实的长时间计算/环境复用场景,再重新评估。**最终包体和完整下载性能目前没有“大幅提升”的证据**。
本 PR 仅交付可复现的调研/原型,不切换默认引擎。生产 engine 原有测试通过,记录在 [production-engine-tests.txt](results/production-engine-tests.txt)。复现命令见 [README.md](README.md)。
-86
View File
@@ -1,86 +0,0 @@
# CPU 与内存补充测量
日期:2026-11-14。沿用 [主报告](REPORT.md) 的引擎版本、三个固定扩展包、回放输入和 M1 / Go 1.26.0 环境。
**建议维持 goja,暂不投入完整替换。** 在当前每次事件新建引擎、重新编译加载的方式下,B 站/YouTube 的 CPU 只减少约 12% / 15%,没有一致的内存优势;连续解析时峰值 RSS 更高。Twitter 的 CPU 降幅较大,但每次只省约 5 ms。这些收益不足以抵消完整宿主桥接、兼容性和维护成本。
## 测量方式
- 每个样本使用新的独立 OS 进程,两个引擎交替先后顺序,5 轮重复,报告中位数;最终数据共 120 个进程样本。测量时未与本次其他构建或测试并发。
- `GOMAXPROCS=1 GOGC=100 GOMEMLIMIT=off`,不使用 PGO。CPU 是 `getrusage(RUSAGE_SELF)` 的 **user + system 时间**,除以完成次数;包含 GC 和编译开销,不使用瞬时 CPU 占用百分比,也不把墙钟耗时直接当 CPU 时间。
- `cold`:一次进程内先预热一次全局初始化,再连续完成 20 次“新建环境、克隆 bundle 输入字符串、编译加载、解析、关闭”。接近当前反复触发扩展事件的方式。每次使用独立 bundle 字符串,模拟生产文件读取后的输入所有权。
- `warm`:环境已加载,连续解析 200 次。仅用于确认复用环境时的 CPU 上限;当前生产事件路径没有这样复用。
- 内存组不预建引擎,从相同测量程序基线开始,分别建立并保留 1 / 8 个完整环境,每个环境执行一次解析后保持存活。8 个环境是同时保留,不是同时并行执行;不模拟实际 Blob/FFmpeg 媒体数据。
- 存活堆:`runtime.MemStats.HeapAlloc` 相对建引擎前的增量。连续 GC 并给异步清理调度机会,至少 3 轮、最多 8 轮,直到相邻值差异小于 64 KiB;不稳定则测量失败。保留源字符串并用 `runtime.KeepAlive` 保证环境仍存活,避免编译器提前释放。
- RSS:通过 `ps` 记录当前物理驻留快照,通过 `getrusage` 记录进程历史峰值;macOS 的峰值单位为字节,Linux 转换为字节。`ps`、稳定 GC 和内存快照都在计时区间外,`RUSAGE_SELF` 不包括 sampler 子进程 CPU。
- 两个引擎链接在相同测量 worker 中,RSS 含公共 Go/测试框架/代码页和 allocator 保留页,**不是完整 Gopeed 应用或最终单引擎产品的内存预测**。存活堆增量用来区分扩展保留对象和进程历史峰值。
- 全部输出按 SHA-256 比较一致,没有通过减少文件或请求处理量获得更好的数字。宿主 mock、HTTP fixture、未移植能力及真实下载尚未验证等限制与主报告相同。
## 冷路径 CPU 与连续解析峰值
| 扩展 | goja CPU (ms/次) | moejs CPU (ms/次) | CPU 减少 | goja 峰值 RSS (MiB) | moejs 峰值 RSS (MiB) |
| --- | ---: | ---: | ---: | ---: | ---: |
| bilibili | 118.962 | 104.081 | 12.5% | 154.7 | 172.3 |
| youtube | 278.476 | 237.330 | 14.8% | 283.8 | 358.9 |
| twitter | 15.555 | 10.594 | 31.9% | 79.7 | 96.8 |
峰值是连续 20 次冷解析期间**整个 worker 的进程峰值**,不是单个 runtime 的堆大小。峰值会受 Go GC/allocator 与异步清理时机影响;不能将它当作关闭后仍存活的对象。原始数据保留每轮值,汇总文件也提供 min/max。
| 扩展 | goja 累计分配 (MiB/次) | moejs 累计分配 (MiB/次) | 分配减少 |
| --- | ---: | ---: | ---: |
| bilibili | 73.69 | 60.17 | 18.3% |
| youtube | 159.91 | 146.82 | 8.2% |
| twitter | 10.96 | 7.73 | 29.5% |
moejs 的累计分配量确实较少,但这没有转化为更低的连续解析峰值 RSS。`B/op`、GC 后存活堆、RSS 是不同指标,不能互相替代。
## 保留环境时的内存
| 扩展 | 保留环境数 | goja 存活堆增量 (MiB) | moejs 存活堆增量 (MiB) | goja RSS 快照 (MiB) | moejs RSS 快照 (MiB) |
| --- | ---: | ---: | ---: | ---: | ---: |
| bilibili | 1 | 15.87 | 15.66 | 114.2 | 111.2 |
| bilibili | 8 | 126.72 | 120.60 | 362.0 | 302.6 |
| youtube | 1 | 32.48 | 41.22 | 203.7 | 192.2 |
| youtube | 8 | 259.64 | 320.54 | 627.8 | 680.1 |
| twitter | 1 | 2.53 | 2.63 | 75.6 | 74.7 |
| twitter | 8 | 20.06 | 19.48 | 106.9 | 93.5 |
B 站一个环境的存活堆几乎没有变化;YouTube 反而更高。多环境情况下也没有稳定、普遍的显著节省。RSS 快照包含 allocator 页面,因此与存活堆的变化比例不同。
最初单次 GC 的 YouTube 堆快照约为 32.5 → 42.2 MiB。考虑 moejs 异步清理编译缓存后,已重测驻留组,表中使用稳定 GC 的结果,不能将初始 42.2 MiB 当成最终驻留值。CPU 和冷路径峰值取自第一次进程资源测量;这些计时和峰值发生在强制 GC 之前,不受驻留测量修正影响。
关闭全部环境并等稳定 GC 后,额外存活 Go 堆回落到 goja 约 0.03–0.05 MiB、moejs 约 0.18–0.97 MiB。这包含一次性进程缓存;本实验没有据此认定泄漏。RSS 不一定同步回落,不能只看关闭后的 RSS 判断对象是否已释放。
## 热路径 CPU
| 扩展 | goja CPU (ms/次) | moejs CPU (ms/次) | CPU 减少 |
| --- | ---: | ---: | ---: |
| bilibili | 19.688 | 3.173 | 83.9% |
| youtube | 2.905 | 0.215 | 92.6% |
| twitter | 3.265 | 0.453 | 86.1% |
热路径的大幅 CPU 节省再次成立。但它要求持有并复用已加载的环境;对当前每个事件重建环境的实现,应该采用上面的 cold 结果评估迁移。
## 决策
按当前扩展和架构,CPU/内存补测没有提供足够的替换依据。尤其 B 站和 YouTube 的 CPU 节省有限,内存没有一致改善,加上主报告里压缩体积几乎不变,应先维持 goja,把本 PR 留作调研记录。
未来若出现长时间执行计算、反复复用环境的真实扩展,再重新评估;若独立研究编译缓存或环境复用,必须同时在 goja 和 moejs 上比较收益,不能把架构优化全部归给引擎替换。
## 复现和证据
```sh
cd experiments/moejs
python3 fetch_sources.py
python3 measure_resources.py > results/resources-run.txt
# 只重新汇总已保存的原始样本,不启动新测量
python3 measure_resources.py --summarize-only
# 必要时只重测驻留组,保留之前的 CPU 样本
python3 measure_resources.py --memory-only > results/resources-memory-run.txt
```
原始样本:[resources.json](results/resources.json);汇总:[resources-summary.json](results/resources-summary.json)。首次资源运行日志:[resources-run.txt](results/resources-run.txt);稳定 GC 后重测驻留组日志:[resources-memory-run.txt](results/resources-memory-run.txt)。首次日志中的驻留组已被后者替代。当前 worker:[resource_worker_test.go](resource_worker_test.go);驱动:[measure_resources.py](measure_resources.py);功能检查:[resources-tests.txt](results/resources-tests.txt)。CPU/warm 组中单次 GC 的快照保留作原始记录,内存结论只使用重新测得的 `retained` 组。
-67
View File
@@ -1,67 +0,0 @@
package evaluation
import (
"encoding/json"
"fmt"
"testing"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/MoeForge/moejs"
)
// Demonstrate an explicit copy-in/copy-out resolve bridge, since FromGo does
// not provide goja's live Go-struct wrappers. Existing task wrappers still need
// native methods and lifecycle ownership before this can ship.
func TestMoeResolveWriteback(t *testing.T) {
r := setup(t, "moejs", "bilibili").(*Moe)
req := &base.Request{URL: "https://www.bilibili.com/video/BV1BJ4m1e7g8?p=2"}
done := make(chan error, 1)
if !r.loop.RunOnLoop(func(rt *moejs.Runtime) { done <- rt.SetGlobal("hostContext", map[string]any{"req": req}) }) {
t.Fatal("loop stopped")
}
if e := <-done; e != nil {
t.Fatal(e)
}
v, e := r.Eval(`(async()=>{await handlers.onResolve(hostContext);
// Goja coerces numeric JS label values into map[string]string. JSON does not.
for (const f of hostContext.res.files) {
if (f.req.labels) f.req.labels = Object.fromEntries(Object.entries(f.req.labels).map(([k,v])=>[k,String(v)]));
}
return JSON.stringify(hostContext.res)})()`)
if e != nil {
t.Fatal(e)
}
var res base.Resource
if e = json.Unmarshal([]byte(fmt.Sprint(v)), &res); e != nil {
t.Fatal(e)
}
if e = res.Validate(); e != nil {
t.Fatal(e)
}
if len(res.Files) != 1 || res.Files[0].Name != "BV1BJ4m1e7g8.真实扩展 Replay 测试(P2).1080p.mp4" {
t.Fatalf("unexpected resource: %+v", res)
}
t.Logf("Go Resource.Validate passed, name=%s", res.Files[0].Name)
}
func TestBufferedReplay(t *testing.T) {
for _, name := range []string{"bilibili", "youtube", "twitter"} {
t.Run(name, func(t *testing.T) {
var want any
for _, engine := range []string{"goja", "moejs"} {
t.Run(engine, func(t *testing.T) {
r := setup(t, engine, name)
v, e := r.Eval("Response = BufferedResponse; resolveOnce()")
if e != nil {
t.Fatal(e)
}
if engine == "goja" {
want = v
} else if v != want {
t.Fatalf("output differs: %v / %v", want, v)
}
})
}
})
}
}
-22
View File
@@ -1,22 +0,0 @@
//go:build !moejs
package main
import (
"fmt"
"github.com/dop251/goja"
"os"
)
func main() {
source := "1+1"
if len(os.Args) > 1 {
source = os.Args[1]
}
rt := goja.New()
v, e := rt.RunString(source)
if e != nil {
panic(e)
}
fmt.Println(v.Export())
}
@@ -1,30 +0,0 @@
//go:build moejs
package main
import (
"fmt"
"github.com/MoeForge/moejs"
"os"
)
func main() {
source := "1+1"
if len(os.Args) > 1 {
source = os.Args[1]
}
rt := moejs.NewRuntime(moejs.Options{MutableIntrinsics: true})
script, e := moejs.CompileScript("size", source)
if e != nil {
panic(e)
}
v, e := rt.RunScript(script)
if e != nil {
panic(e)
}
out, e := rt.ToGo(v)
if e != nil {
panic(e)
}
fmt.Println(out)
}
-139
View File
@@ -1,139 +0,0 @@
package evaluation
import (
"fmt"
"os"
"testing"
)
func source(t testing.TB, name string) string {
t.Helper()
b, e := os.ReadFile("testdata/extensions/" + name + ".js")
if e != nil {
t.Fatal("run python3 fetch_sources.py first: ", e)
}
return string(b)
}
func setup(t testing.TB, engine, name string) Runtime {
t.Helper()
r, e := New(engine)
if e != nil {
t.Fatal(e)
}
t.Cleanup(r.Close)
b, e := os.ReadFile("replay.js")
if e != nil {
t.Fatal(e)
}
for _, s := range []string{fmt.Sprintf("var extensionName = %q;", name), string(b), source(t, name)} {
if _, e = r.Eval(s); e != nil {
t.Fatal(e)
}
}
return r
}
func TestExtensionReplay(t *testing.T) {
for _, name := range []string{"bilibili", "youtube", "twitter"} {
t.Run(name, func(t *testing.T) {
var want any
for _, engine := range []string{"goja", "moejs"} {
t.Run(engine, func(t *testing.T) {
r := setup(t, engine, name)
v, e := r.Eval("resolveOnce()")
if e != nil {
t.Fatal(e)
}
t.Log(v)
if engine == "goja" {
want = v
} else if want != nil && v != want {
t.Fatalf("output differs\ngoja: %s\nmoejs: %s", want, v)
}
})
}
})
}
}
func BenchmarkExtension(b *testing.B) {
for _, name := range []string{"bilibili", "youtube", "twitter"} {
for _, engine := range []string{"goja", "moejs"} {
b.Run(name+"/"+engine+"/warm", func(b *testing.B) {
r := setup(b, engine, name)
if _, e := r.Eval("resolveOnce()"); e != nil {
b.Fatal(e)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, e := r.Eval("resolveOnce()"); e != nil {
b.Fatal(e)
}
}
})
b.Run(name+"/"+engine+"/cold", func(b *testing.B) {
src := source(b, name)
p, e := os.ReadFile("replay.js")
if e != nil {
b.Fatal(e)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r, e := New(engine)
if e != nil {
b.Fatal(e)
}
for _, s := range []string{fmt.Sprintf("var extensionName=%q;", name), string(p), src, "resolveOnce()"} {
if _, e = r.Eval(s); e != nil {
r.Close()
b.Fatal(e)
}
}
r.Close()
}
})
}
}
}
func TestExtensionLifecycle(t *testing.T) {
for _, name := range []string{"bilibili", "youtube", "twitter"} {
t.Run(name, func(t *testing.T) {
var want any
for _, engine := range []string{"goja", "moejs"} {
t.Run(engine, func(t *testing.T) {
r := setup(t, engine, name)
v, e := r.Eval("lifecycleOnce()")
if e != nil {
t.Fatal(e)
}
t.Log(v)
if engine == "goja" {
want = v
} else if want != nil && v != want {
t.Fatalf("lifecycle differs\ngoja: %s\nmoejs: %s", want, v)
}
})
}
})
}
}
func BenchmarkBufferedTransport(b *testing.B) {
for _, name := range []string{"bilibili", "youtube", "twitter"} {
for _, engine := range []string{"goja", "moejs"} {
b.Run(name+"/"+engine, func(b *testing.B) {
r := setup(b, engine, name)
if _, e := r.Eval("Response = BufferedResponse; resolveOnce()"); e != nil {
b.Fatal(e)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, e := r.Eval("resolveOnce()"); e != nil {
b.Fatal(e)
}
}
})
}
}
}
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env python3
"""Fetch the unmodified published extension bundles at immutable commits."""
import hashlib
import json
from pathlib import Path
import subprocess
import tempfile
sources = {
'bilibili': ('monkeyWie/gopeed-extension-bilibili', 'b91db1ea52e975d8de9823f01a7af5f9ea83792f'),
'youtube': ('monkeyWie/gopeed-extension-youtube', 'eeb78d168e053e9a2aad1dbaa309fc11f6992f7c'),
'twitter': ('monkeyWie/gopeed-extension-twitter', 'a963428bbfdbee9510bf224b00b384e37514ce77'),
}
root = Path(__file__).resolve().parent / 'testdata' / 'extensions'
root.mkdir(parents=True, exist_ok=True)
expected = json.loads((root.parent.parent / 'results/sources.json').read_text())
manifest = {}
for name, (repo, commit) in sources.items():
url = f'https://raw.githubusercontent.com/{repo}/{commit}/dist/index.js'
with tempfile.TemporaryDirectory(prefix='gopeed-extension-') as checkout:
subprocess.run(['git', 'init', '-q', checkout], check=True)
subprocess.run(['git', '-C', checkout, 'fetch', '-q', '--depth=1', f'https://github.com/{repo}.git', commit], check=True)
data = subprocess.check_output(['git', '-C', checkout, 'show', 'FETCH_HEAD:dist/index.js'])
digest = hashlib.sha256(data).hexdigest()
if digest != expected[name]['sha256']:
raise RuntimeError(f'Unexpected bundle hash for {name}: {digest}')
(root / f'{name}.js').write_bytes(data)
manifest[name] = dict(repo=repo, commit=commit, url=url, bytes=len(data), sha256=hashlib.sha256(data).hexdigest())
print(name, len(data), manifest[name]['sha256'])
(root / 'sources.json').write_text(json.dumps(manifest, indent=2) + '\n')
-16
View File
@@ -1,16 +0,0 @@
package evaluation
import "testing"
func TestFrozenExtensionReplay(t *testing.T) {
for _, name := range []string{"bilibili", "youtube", "twitter"} {
t.Run(name, func(t *testing.T) {
r := setup(t, "moejs-frozen", name)
v, e := r.Eval("resolveOnce()")
if e != nil {
t.Fatal(e)
}
t.Log(v)
})
}
}
-35
View File
@@ -1,35 +0,0 @@
module github.com/GopeedLab/gopeed/experiments/moejs
go 1.26.0
replace github.com/GopeedLab/gopeed => ../..
require (
github.com/GopeedLab/gopeed v0.0.0
github.com/MoeForge/moejs v0.0.0-20261113160646-b2c7fed9c42d
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
)
require (
codeberg.org/gruf/go-ffmpreg v0.6.23-0.20260827095416-df1836cb5be0 // indirect
github.com/MoeForge/moego v0.1.0-alpha.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/imroc/req/v3 v3.58.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-ieproxy v0.0.12 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
github.com/tetratelabs/wazero v1.12.0 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect
)
-68
View File
@@ -1,68 +0,0 @@
codeberg.org/gruf/go-ffmpreg v0.6.23-0.20260827095416-df1836cb5be0 h1:AqNFl60o4xhTTOxmzAvSf7pazXHKBGuV5ATw7sETIRA=
codeberg.org/gruf/go-ffmpreg v0.6.23-0.20260827095416-df1836cb5be0/go.mod h1:XM6eW8CSgAuZRKRynm5nDiSNedm0rav30IychqygOHE=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/MoeForge/moego v0.1.0-alpha.1 h1:d+asBZu+2oKnBj1kzdvdANMStbIKw9BLCpknuWjiR28=
github.com/MoeForge/moego v0.1.0-alpha.1/go.mod h1:SBNiQjCpWC3IEBMKFBOwOyhhQ3jywMrISqCwJTdr4aA=
github.com/MoeForge/moejs v0.0.0-20261113160646-b2c7fed9c42d h1:aM5l0SGzQaE3rPvqskuukTL8Iv1wKt+kBq06NjfSlm0=
github.com/MoeForge/moejs v0.0.0-20261113160646-b2c7fed9c42d/go.mod h1:GDaH0vQSxKDN7FCyBaLEZgqsGKRyIYBfje/p74aZQ3k=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 h1:bVp3yUzvSAJzu9GqID+Z96P+eu5TKnIMJSV4QaZMauM=
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co=
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.58.0 h1:rBqJhv2gZjMymGot4vKmNSUs/mAITMpGASqElhuMvS4=
github.com/imroc/req/v3 v3.58.0/go.mod h1:rC7NTGgfMSkVWR4GO46rMeJsUfaeOpa6pLsgpq9q3zI=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/mattn/go-ieproxy v0.0.12 h1:OZkUFJC3ESNZPQ+6LzC3VJIFSnreeFLQyqvBWtvfL2M=
github.com/mattn/go-ieproxy v0.0.12/go.mod h1:Vn+N61199DAnVeTgaF8eoB9PvLO8P3OBnG95ENh7B7c=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
-59
View File
@@ -1,59 +0,0 @@
package evaluation
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestAsyncHTTP(t *testing.T) {
cancelled := make(chan struct{}, 4)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/wait" {
<-r.Context().Done()
cancelled <- struct{}{}
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"value":42}`)
}))
defer server.Close()
for _, engine := range []string{"goja", "moejs"} {
t.Run(engine, func(t *testing.T) {
r, e := newRuntime(engine, true)
if e != nil {
t.Fatal(e)
}
defer r.Close()
v, e := r.Eval(fmt.Sprintf(`(async()=>{const r=await fetch(%q);return (await r.json()).value})()`, server.URL))
if e != nil || fmt.Sprint(v) != "42" {
t.Fatalf("fetch value=%v, error=%v", v, e)
}
v, e = r.Eval(fmt.Sprintf(`(async()=>{const c=new AbortController();const p=fetch(%q,{signal:c.signal});setTimeout(()=>c.abort(new Error('evaluation cancel')),10);try{await p;return false}catch(e){return e.name+': '+e.message}})()`, server.URL+"/wait"))
want := "evaluation cancel"
// Existing Gopeed fetch catch path constructs DOMException without a
// definition in this standalone host. Record this baseline divergence;
// independently require actual cancellation of the server request below.
if engine == "goja" {
want = "ReferenceError: DOMException is not defined"
}
if e != nil || !strings.Contains(fmt.Sprint(v), want) {
t.Fatalf("abort value=%v, error=%v", v, e)
}
t.Logf("abort rejection: %v", v)
select {
case <-cancelled:
case <-time.After(2 * time.Second):
t.Fatal("request context was not cancelled")
}
probe, e := r.Eval(`(()=>{try{const c=new AbortController();c.abort();return c.signal.reason?.name}catch(e){return e.name+': '+e.message}})()`)
if e != nil {
t.Fatal(e)
}
t.Logf("default abort probe: %v", probe)
})
}
}
-107
View File
@@ -1,107 +0,0 @@
package evaluation
import (
"fmt"
"testing"
"github.com/MoeForge/moejs"
"github.com/dop251/goja"
)
type request struct {
URL string `json:"url"`
}
type context struct {
Req *request `json:"req"`
Res map[string]any `json:"res"`
}
func (r *request) SetURL(s string) { r.URL = s }
// These are migration probes, asserting the observed difference, not claiming
// the existing Gopeed Go bridge has been ported.
func TestHostInterop(t *testing.T) {
for _, name := range []string{"goja", "moejs"} {
t.Run(name, func(t *testing.T) {
c := &context{Req: &request{URL: "before"}}
code := `ctx.req.url = "after"; ctx.res = {files:[{name:"video.mp4"}]}; typeof ctx.req.setURL`
var v any
var e error
if name == "goja" {
rt := goja.New()
rt.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
if e = rt.Set("ctx", c); e != nil {
t.Fatal(e)
}
value, err := rt.RunString(code)
e = err
if e == nil {
v = value.Export()
}
if c.Req.URL != "after" || c.Res == nil {
t.Fatal("goja write through failed")
}
} else {
rt := moejs.NewRuntime(moejs.Options{MutableIntrinsics: true})
if e = rt.SetGlobal("ctx", c); e != nil {
t.Fatal(e)
}
s, err := moejs.CompileScript("interop", code)
if err != nil {
t.Fatal(err)
}
value, err := rt.RunScript(s)
e = err
if e == nil {
v, e = rt.ToGo(value)
}
if c.Req.URL != "before" || c.Res != nil {
t.Fatal("expected snapshot semantics")
}
}
if e != nil {
t.Fatal(e)
}
t.Logf("Go URL=%s, Go res=%v, JS method=%v", c.Req.URL, c.Res, v)
})
}
}
func TestFrozenCompatibility(t *testing.T) {
r, e := New("moejs-frozen")
if e != nil {
t.Logf("shared frozen builtins reject host polyfill: %v", e)
return
}
defer r.Close()
b, e := r.Eval(`Object.defineProperty(Array.prototype,"__probe",{value:1}); true`)
if e == nil {
t.Fatalf("expected frozen builtins to reject mutation, got %v", b)
}
t.Log(e)
}
func BenchmarkBareRuntime(b *testing.B) {
for _, name := range []string{"goja", "moejs-mutable", "moejs-shared"} {
b.Run(name, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if name == "goja" {
r := goja.New()
v, e := r.RunString("1+1")
if e != nil || v.ToInteger() != 2 {
b.Fatal(e)
}
} else {
r := moejs.NewRuntime(moejs.Options{MutableIntrinsics: name == "moejs-mutable"})
s, e := moejs.CompileScript("small", "1+1")
if e != nil {
b.Fatal(e)
}
v, e := r.RunScript(s)
if e != nil || v.String() != "2" {
b.Fatal(fmt.Sprint(v), e)
}
}
}
})
}
}
-49
View File
@@ -1,49 +0,0 @@
package evaluation
import (
"fmt"
"os"
"testing"
"time"
)
// Opt in: public endpoints are volatile and are not used in benchmark timings.
// Only URL resolution is attempted; blob registration and UA WebView stay mocked.
func TestLiveResolve(t *testing.T) {
if os.Getenv("GOPEED_MOEJS_LIVE") != "1" {
t.Skip("set GOPEED_MOEJS_LIVE=1 to contact public endpoints")
}
for _, name := range []string{"bilibili", "youtube", "twitter"} {
var want any
for _, engine := range []string{"goja", "moejs"} {
t.Run(name+"/"+engine, func(t *testing.T) {
r, e := newRuntime(engine, true)
if e != nil {
t.Fatal(e)
}
defer r.Close()
p, e := os.ReadFile("replay.js")
if e != nil {
t.Fatal(e)
}
for _, s := range []string{fmt.Sprintf("var extensionName=%q;", name), string(p), "liveNetwork = true;", source(t, name)} {
if _, e = r.Eval(s); e != nil {
t.Fatal(e)
}
}
started := time.Now()
v, e := r.Eval("resolveOnce()")
t.Logf("elapsed=%s", time.Since(started))
if e != nil {
t.Fatal(e)
}
if engine == "goja" {
want = v
} else if want != nil && v != want {
t.Fatalf("live resolve outputs differ: %v / %v", want, v)
}
t.Log(v)
})
}
}
}
-88
View File
@@ -1,88 +0,0 @@
#!/usr/bin/env python3
"""Measure equal extension work in fresh OS processes, never CPU percentage."""
import argparse
import json
import os
from pathlib import Path
import statistics
import subprocess
import tempfile
root = Path(__file__).resolve().parent
os.chdir(root)
parser = argparse.ArgumentParser()
parser.add_argument('--repeats', type=int, default=5)
options = parser.add_mutually_exclusive_group()
options.add_argument('--smoke', action='store_true')
options.add_argument('--memory-only', action='store_true', help='replace retained samples, keeping saved CPU samples')
options.add_argument('--summarize-only', action='store_true')
args = parser.parse_args()
if args.repeats < 1:
parser.error('--repeats must be positive')
all_workloads = [('cold',20),('warm',200),('retained',1),('retained',8)]
workloads = [('cold',2),('warm',2),('retained',2)] if args.smoke else all_workloads
filename = 'resources-smoke.json' if args.smoke else 'resources.json'
rows = []
if args.summarize_only:
rows = json.loads((root/'results'/filename).read_text())
else:
(root/'bin').mkdir(exist_ok=True)
binary = root/'bin/resource-worker.test'
subprocess.run(['go','test','-pgo=off','-c','-o',str(binary)],check=True)
if args.memory_only:
rows = [r for r in json.loads((root/'results'/filename).read_text()) if r['mode'] != 'retained']
workloads = [('retained',1),('retained',8)]
with tempfile.TemporaryDirectory(prefix='gopeed-resource-') as temp:
for extension in ['bilibili','youtube','twitter']:
for mode,count in workloads:
for repeat in range(args.repeats):
engines = ['goja','moejs'] if repeat % 2 == 0 else ['moejs','goja']
for engine in engines:
output = Path(temp)/'result.json'
env = {**os.environ,'GOMAXPROCS':'1','GOGC':'100','GOMEMLIMIT':'off',
'GOPEED_RESOURCE_WORKER':'1','RESOURCE_ENGINE':engine,
'RESOURCE_EXTENSION':extension,'RESOURCE_MODE':mode,
'RESOURCE_COUNT':str(count),'RESOURCE_OUTPUT':str(output)}
run = subprocess.run([str(binary),'-test.run=^TestResourceWorker$','-test.timeout=90s'],
env=env,text=True,capture_output=True,timeout=100)
if run.returncode:
raise RuntimeError(run.stdout+run.stderr)
row = json.loads(output.read_text())
row['repeat'] = repeat
rows.append(row)
hashes = {r['output_sha256'] for r in rows if r['extension']==extension}
if len(hashes)!=1:
raise RuntimeError(f'Output mismatch for {extension}: {hashes}')
cpu = (row['user_cpu_ns']+row['system_cpu_ns'])/count/1e6
print(f'{extension}/{mode}/{count}/{engine} repeat={repeat+1}: '
f'CPU={cpu:.3f} ms/op, peakRSS={row["after_work"]["process_peak_rss_bytes"]/2**20:.1f} MiB',flush=True)
(root/'results'/filename).write_text(json.dumps(rows,indent=2)+'\n')
summary = []
for extension in ['bilibili','youtube','twitter']:
for mode,count in (all_workloads if args.memory_only else workloads):
for engine in ['goja','moejs']:
group = [r for r in rows if (r['extension'],r['mode'],r['count'],r['engine']) == (extension,mode,count,engine)]
if not group:
raise RuntimeError(f'Missing samples for {extension}/{mode}/{count}/{engine}')
def med(fn): return statistics.median(fn(r) for r in group)
cpu = [(r['user_cpu_ns']+r['system_cpu_ns'])/count for r in group]
summary.append(dict(extension=extension,mode=mode,count=count,engine=engine,samples=len(group),
cpu_ns_per_op=statistics.median(cpu),cpu_ns_min=min(cpu),cpu_ns_max=max(cpu),
user_cpu_ns_per_op=med(lambda r:r['user_cpu_ns']/count),
system_cpu_ns_per_op=med(lambda r:r['system_cpu_ns']/count),
wall_ns_per_op=med(lambda r:r['wall_ns']/count),
allocated_bytes_per_op=med(lambda r:r['allocated_bytes']/count),
gc_cycles=med(lambda r:r['gc_cycles']),
peak_rss_bytes=med(lambda r:r['after_work']['process_peak_rss_bytes']),
peak_rss_min_bytes=min(r['after_work']['process_peak_rss_bytes'] for r in group),
peak_rss_max_bytes=max(r['after_work']['process_peak_rss_bytes'] for r in group),
rss_after_work_bytes=med(lambda r:r['after_work']['rss_bytes']),
live_heap_bytes=med(lambda r:r['after_gc']['heap_alloc_bytes']),
live_heap_delta_bytes=med(lambda r:r['after_gc']['heap_alloc_bytes']-r['baseline']['heap_alloc_bytes']),
retained_rss_bytes=med(lambda r:r['after_os_release']['rss_bytes']),
retained_rss_delta_bytes=med(lambda r:r['after_os_release']['rss_bytes']-r['baseline']['rss_bytes']),
after_close_heap_delta_bytes=med(lambda r:r['after_close_os_release']['heap_alloc_bytes']-r['baseline']['heap_alloc_bytes']),
after_close_rss_bytes=med(lambda r:r['after_close_os_release']['rss_bytes'])))
name = 'resources-smoke-summary.json' if args.smoke else 'resources-summary.json'
(root/'results'/name).write_text(json.dumps(summary,indent=2)+'\n')
-62
View File
@@ -1,62 +0,0 @@
#!/usr/bin/env python3
"""Build stripped interpreter probes plus real CLI baseline/coexistence."""
import gzip
import json
import os
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parent
os.chdir(root)
output = root / 'bin'
output.mkdir(exist_ok=True)
rows = []
for target in [('darwin','arm64'),('linux','amd64'),('windows','amd64'),('android','arm64')]:
goos, arch = target
env = {**os.environ, 'GOOS':goos, 'GOARCH':arch, 'CGO_ENABLED':'0'}
for engine in ['goja','moejs']:
path = output / f'{goos}-{arch}-{engine}'
cmd = ['go','build','-trimpath','-pgo=off','-ldflags=-s -w','-o',str(path)]
if engine == 'moejs':
cmd += ['-tags=moejs']
subprocess.run(cmd+['./cmd/engine-size'], env=env, check=True)
data=path.read_bytes()
rows.append(dict(scope='interpreter-only',target=f'{goos}/{arch}',engine=engine,bytes=len(data),gzip_bytes=len(gzip.compress(data,mtime=0))))
print(rows[-1], flush=True)
# No fake replacement: this is the actual CLI, with and without linking the
# candidate runtime. The overlay does not change the root module or source.
with tempfile.TemporaryDirectory(prefix='gopeed-moejs-size-') as tmp:
tmp=Path(tmp)
extra=tmp/'link.go'
extra.write_text('''package main
import (
"github.com/MoeForge/moejs"
"github.com/MoeForge/moejs/eventloop"
"github.com/MoeForge/moejs/webapi"
)
func init() {
rt:=moejs.NewRuntime(moejs.Options{MutableIntrinsics:true})
l,e:=eventloop.New(rt,eventloop.Options{});if e!=nil{panic(e)}
api,e:=webapi.Install(l,webapi.Options{});if e!=nil{panic(e)}
s,e:=moejs.CompileScript("probe","1+1");if e!=nil{panic(e)}
_,e=rt.RunScript(s);if e!=nil{panic(e)}
api.Close();l.Terminate()
}
''')
overlay=tmp/'overlay.json'
overlay.write_text(json.dumps({'Replace':{str(root.parent.parent/'cmd/gopeed/moejs_eval_link.go'):str(extra)}}))
for goos,arch in [('darwin','arm64'),('linux','amd64'),('windows','amd64')]:
env={**os.environ,'GOOS':goos,'GOARCH':arch,'CGO_ENABLED':'0'}
for engine in ['goja-production','goja-plus-moejs-webapi']:
path=output/f'cli-{goos}-{arch}-{engine}'
cmd=['go','build','-mod=mod','-trimpath','-pgo=off','-tags=nosqlite','-ldflags=-s -w','-o',str(path)]
if engine.endswith('webapi'):
cmd += ['-overlay',str(overlay)]
subprocess.run(cmd+['github.com/GopeedLab/gopeed/cmd/gopeed'],env=env,check=True)
data=path.read_bytes()
rows.append(dict(scope='real-cli',target=f'{goos}/{arch}',engine=engine,bytes=len(data),gzip_bytes=len(gzip.compress(data,mtime=0))))
print(rows[-1],flush=True)
(root/'results/size.json').write_text(json.dumps(rows,indent=2)+'\n')
# CLI builds may add sums/indirect requirements in this isolated module.
subprocess.run(['go','mod','tidy'],check=True)
-84
View File
@@ -1,84 +0,0 @@
// Synthetic HTTP responses at the transport boundary. Entire upstream bundles
// (including SDKs) execute unchanged; this is not a live website benchmark.
var nativeFetch = fetch;
var liveNetwork = false;
var requests = [];
var handlers = {};
var blobSerial = 0;
var gopeed = {
info: {identity: 'monkeyWie@' + extensionName},
host: {env:{version:'2.0.0-beta.3',os:'darwin',arch:'arm64'}},
settings:{quality:extensionName==='youtube'?'1080p':80,qualityFallback:'best',fallbackToBest:true,cookie:''},
logger:{debug(){},info(){},warn(){},error(){}},
storage:{get(){return null},set(){}},
events:Object.fromEntries(['onResolve','onStart','onError','onDone'].map(k=>[k,fn=>handlers[k]=fn])),
runtime:{
ffmpeg:{merge(){throw new Error('media merge is outside this replay')},supportsInputFactory:true,supportsProducerProgress:true},
blob:{async createObjectURL(){return 'blob:replay/'+(++blobSerial)},async revokeObjectURL(){}},
webview:{isAvailable(){return true},async open(){return {
async goto(){},async execute(expression){
if (expression !== '() => navigator.userAgent') throw new Error('unexpected WebView execution');
return 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.0 Safari/605.1.15';
},async close(){},async getCookies(){return []},async setCookie(){},async deleteCookie(){}
}}}
}};
class MessageError extends Error {}
var route = (raw, init) => {
const url = new URL(String(raw));
requests.push(url.pathname);
const json = x => new Response(JSON.stringify(x),{status:200,headers:{'content-type':'application/json'}});
if (url.pathname === '/x/web-interface/nav') return json({code:0,data:{wbi_img:{img_url:'https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png',sub_url:'https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png'}}});
if (url.pathname === '/x/web-interface/wbi/view' || url.pathname === '/x/web-interface/view') return json({code:0,data:{title:'真实扩展 Replay 测试',pages:Array.from({length:3},(_,i)=>({cid:100+i,part:'P'+(i+1)}))}});
if (url.pathname === '/x/player/wbi/playurl' || url.pathname === '/x/player/playurl') return json({code:0,data:{quality:80,format:'dash',dash:{video:[{id:64,width:1280,height:720,baseUrl:'https://upos-test.bilivideo.com/video64'},{id:80,width:1920,height:1080,baseUrl:'https://upos-test.bilivideo.com/video80',backupUrl:['https://backup.example.com/video80']}],audio:[{id:30280,baseUrl:'https://upos-test.bilivideo.com/audio'}]}}});
if (url.pathname === '/youtubei/v1/player') return json({responseContext:{serviceTrackingParams:[]},playabilityStatus:{status:'OK'},videoDetails:{videoId:'dQw4w9WgXcQ',title:'YouTube Replay 测试',lengthSeconds:'212',channelId:'UCfixture',shortDescription:'Replay fixture',viewCount:'100',author:'Fixture',isLiveContent:false},microformat:{playerMicroformatRenderer:{title:{simpleText:'YouTube Replay 测试'},description:{simpleText:'Replay'},category:'Music',publishDate:'2026-11-01',uploadDate:'2026-11-01',isFamilySafe:true}},streamingData:{expiresInSeconds:'3600',adaptiveFormats:[]}});
if (url.pathname.endsWith('/guest/activate.json')) return json({guest_token:'fixture-guest'});
if (url.hostname === 'pastebin.com' && url.pathname === '/raw/nz3ApKQM') return new Response('Bearer fixture-only', {status:200});
if (url.pathname.includes('/TweetResultByRestId')) {
const media = [{type:'photo',media_url_https:'https://pbs.twimg.com/media/replay.jpg'}, {type:'video',media_url_https:'https://pbs.twimg.com/media/replay.jpg',video_info:{duration_millis:10000,variants:[{bitrate:256000,content_type:'video/mp4',url:'https://video.twimg.com/640x360/low.mp4'},{bitrate:2048000,content_type:'video/mp4',url:'https://video.twimg.com/1920x1080/high.mp4'}]}}];
return json({data:{tweetResult:{result:{__typename:'Tweet',views:{count:'100'},core:{user_results:{result:{legacy:{screen_name:'fixture',name:'Replay'}}}},legacy:{id_str:'1734145541599383880',full_text:'Replay',created_at:'Tue Dec 12 00:00:00 +0000 2023',entities:{media}}}}}});
}
if (url.pathname === '/sw.js_data' || url.pathname === '/youtubei/v1/config') return new Response('Offline session: exercise YouTube.js local fallback', {status:503});
throw new Error('unmatched replay request: '+url);
};
fetch = async (input,init) => liveNetwork ? nativeFetch(input,init) : route(input?.url || input,init);
async function resolveOnce() {
requests = [];
blobSerial = 0;
const url = globalThis.inputURL || {bilibili:'https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3',youtube:'https://www.youtube.com/watch?v=dQw4w9WgXcQ',twitter:'https://x.com/bbcchinese/status/1734145541599383880'}[extensionName];
const ctx = {req:{url}};
await handlers.onResolve(ctx);
if (!ctx.res?.files?.length) throw new Error('resolve returned no files');
return JSON.stringify({res:ctx.res,requests});
}
async function lifecycleOnce() {
const resolved = JSON.parse(await resolveOnce());
let continued = 0;
for (const file of resolved.res.files) {
const req = file.req;
req.putLabel = async (key,value) => { (req.labels ||= {})[key] = value; };
req.setUrl = async url => { req.url = url; };
const task = {meta:{req},status:'ready',setUrl:req.setUrl,async continue(){continued++}};
if (handlers.onStart) await handlers.onStart({task});
task.status = 'error';
if (handlers.onError) await handlers.onError({task});
}
return JSON.stringify({res:resolved.res,continued,requests});
}
// Optional common buffered transport to separate SDK work from each runtime's
// Response/ReadableStream implementation. Both engines execute this same JS.
class BufferedResponse {
constructor(text,options={}) {
this._text = String(text); this.status = options.status ?? 200;
this.ok = this.status >= 200 && this.status < 300;
this.statusText = this.ok ? 'OK' : 'Error'; this.url = '';
this.headers = new Headers(options.headers);
const bytes = new TextEncoder().encode(this._text);
this.body = {getReader(){let sent=false;return {async read(){if(sent)return {done:true};sent=true;return {done:false,value:bytes}},async cancel(){},releaseLock(){}}},async cancel(){}};
}
async text(){return this._text}
async json(){return JSON.parse(this._text)}
async arrayBuffer(){return new TextEncoder().encode(this._text).buffer}
clone(){return new BufferedResponse(this._text,{status:this.status,headers:this.headers})}
}
-232
View File
@@ -1,232 +0,0 @@
//go:build darwin || linux
package evaluation
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"os/exec"
"runtime"
"runtime/debug"
"strconv"
"strings"
"syscall"
"testing"
"time"
)
type resourceSnapshot struct {
HeapAlloc uint64 `json:"heap_alloc_bytes"`
HeapInuse uint64 `json:"heap_inuse_bytes"`
HeapObjects uint64 `json:"heap_objects"`
RSS int64 `json:"rss_bytes"`
PeakRSS int64 `json:"process_peak_rss_bytes"`
Goroutines int `json:"goroutines"`
}
type resourceResult struct {
Engine string `json:"engine"`
Extension string `json:"extension"`
Mode string `json:"mode"`
Count int `json:"count"`
UserNS int64 `json:"user_cpu_ns"`
SystemNS int64 `json:"system_cpu_ns"`
WallNS int64 `json:"wall_ns"`
Allocated uint64 `json:"allocated_bytes"`
GCs uint32 `json:"gc_cycles"`
GCPause uint64 `json:"gc_pause_ns"`
OutputHash string `json:"output_sha256"`
Baseline resourceSnapshot `json:"baseline"`
AfterWork resourceSnapshot `json:"after_work"`
AfterGC resourceSnapshot `json:"after_gc"`
AfterOSRelease resourceSnapshot `json:"after_os_release"`
AfterClose resourceSnapshot `json:"after_close_os_release"`
}
// moejs releases weak-keyed compiled metadata via asynchronous Go cleanups.
// Multiple collections with scheduling opportunities avoid counting objects
// pending those cleanups as permanently live. This is outside CPU timing.
func settledGC(t *testing.T) {
t.Helper()
var previous uint64
for i := 0; i < 8; i++ {
runtime.GC()
time.Sleep(20 * time.Millisecond)
var m runtime.MemStats
runtime.ReadMemStats(&m)
delta := m.HeapAlloc
if previous > m.HeapAlloc {
delta = previous - m.HeapAlloc
} else {
delta = m.HeapAlloc - previous
}
if i >= 2 && delta < 64*1024 {
return
}
previous = m.HeapAlloc
}
t.Fatal("live heap did not settle within eight GC rounds")
}
func selfUsage(t *testing.T) syscall.Rusage {
t.Helper()
var usage syscall.Rusage
if err := syscall.Getrusage(syscall.RUSAGE_SELF, &usage); err != nil {
t.Fatal(err)
}
return usage
}
func timevalNS(v syscall.Timeval) int64 { return int64(v.Sec)*1e9 + int64(v.Usec)*1e3 }
func snapshot(t *testing.T) resourceSnapshot {
t.Helper()
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
// ps runs only outside the timed CPU interval. RUSAGE_SELF excludes its CPU.
b, err := exec.Command("ps", "-o", "rss=", "-p", strconv.Itoa(os.Getpid())).Output()
if err != nil {
t.Fatal(err)
}
rss, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64)
if err != nil {
t.Fatal(err)
}
peak := int64(selfUsage(t).Maxrss)
if runtime.GOOS == "linux" {
peak *= 1024
}
return resourceSnapshot{mem.HeapAlloc, mem.HeapInuse, mem.HeapObjects, rss * 1024, peak, runtime.NumGoroutine()}
}
func resourceRuntime(t *testing.T, engine, name, src, host string) Runtime {
t.Helper()
r, err := New(engine)
if err != nil {
t.Fatal(err)
}
// Clone the bundle for each runtime, like a fresh production file read.
for _, s := range []string{fmt.Sprintf("var extensionName=%q;", name), host, strings.Clone(src)} {
if _, err = r.Eval(s); err != nil {
r.Close()
t.Fatal(err)
}
}
return r
}
func resolveResource(t *testing.T, r Runtime) string {
t.Helper()
value, err := r.Eval("resolveOnce()")
if err != nil {
t.Fatal(err)
}
text, ok := value.(string)
if !ok {
t.Fatalf("unexpected resolve result %T", value)
}
sum := sha256.Sum256([]byte(text))
return hex.EncodeToString(sum[:])
}
// Each invocation is a fresh OS process. The Python driver repeats both engines
// in alternating order; the ordinary test suite skips this measurement worker.
func TestResourceWorker(t *testing.T) {
if os.Getenv("GOPEED_RESOURCE_WORKER") != "1" {
t.Skip("process resource measurement worker")
}
engine, name, mode := os.Getenv("RESOURCE_ENGINE"), os.Getenv("RESOURCE_EXTENSION"), os.Getenv("RESOURCE_MODE")
count, err := strconv.Atoi(os.Getenv("RESOURCE_COUNT"))
if err != nil || count < 1 || count > 10000 {
t.Fatal("invalid RESOURCE_COUNT")
}
src := source(t, name)
hostBytes, err := os.ReadFile("replay.js")
if err != nil {
t.Fatal(err)
}
host := string(hostBytes)
// Pay one-time global initialization only before timing repeat events.
// Retained-memory runs start without a priming runtime so their memory
// includes the selected engine's first-use caches and builtins.
var hash string
if mode != "retained" {
prime := resourceRuntime(t, engine, name, src, host)
hash = resolveResource(t, prime)
prime.Close()
prime = nil
}
debug.FreeOSMemory()
result := resourceResult{Engine: engine, Extension: name, Mode: mode, Count: count, Baseline: snapshot(t)}
var retained []Runtime
if mode == "warm" {
r := resourceRuntime(t, engine, name, src, host)
if h := resolveResource(t, r); h != hash {
t.Fatal("warm output differs")
}
retained = append(retained, r)
debug.FreeOSMemory()
}
var before, after runtime.MemStats
runtime.ReadMemStats(&before)
cpuBefore := selfUsage(t)
started := time.Now()
for i := 0; i < count; i++ {
var r Runtime
switch mode {
case "warm":
r = retained[0]
case "cold", "retained":
r = resourceRuntime(t, engine, name, src, host)
default:
t.Fatal("unknown RESOURCE_MODE")
}
h := resolveResource(t, r)
if hash == "" {
hash = h
} else if h != hash {
t.Fatal("resolve output changed")
}
if mode == "retained" {
retained = append(retained, r)
} else if mode == "cold" {
r.Close()
}
}
result.WallNS = time.Since(started).Nanoseconds()
cpuAfter := selfUsage(t)
runtime.ReadMemStats(&after)
result.UserNS = timevalNS(cpuAfter.Utime) - timevalNS(cpuBefore.Utime)
result.SystemNS = timevalNS(cpuAfter.Stime) - timevalNS(cpuBefore.Stime)
result.Allocated = after.TotalAlloc - before.TotalAlloc
result.GCs = after.NumGC - before.NumGC
result.GCPause = after.PauseTotalNs - before.PauseTotalNs
result.AfterWork = snapshot(t)
settledGC(t)
result.AfterGC = snapshot(t)
debug.FreeOSMemory()
result.AfterOSRelease = snapshot(t)
runtime.KeepAlive(retained)
for _, r := range retained {
r.Close()
}
retained = nil
settledGC(t)
debug.FreeOSMemory()
result.AfterClose = snapshot(t)
// Keep input strings alive across all snapshots, including after closing
// runtimes, so memory deltas cannot accidentally subtract the input bundle.
runtime.KeepAlive(src)
runtime.KeepAlive(host)
result.OutputHash = hash
output, err := json.MarshalIndent(result, "", " ")
if err != nil {
t.Fatal(err)
}
if err = os.WriteFile(os.Getenv("RESOURCE_OUTPUT"), append(output, '\n'), 0644); err != nil {
t.Fatal(err)
}
}
@@ -1 +0,0 @@
be0a395447d788419e83acdfd693e48a1a1706bf
@@ -1,170 +0,0 @@
{
"BenchmarkExtension/bilibili/goja/warm": {
"samples": 5,
"ns_median": 21188099.0,
"ns_min": 19941715.0,
"ns_max": 21722030.0,
"bytes_median": 8340744.0,
"allocs_median": 143394.0
},
"BenchmarkExtension/bilibili/goja/cold": {
"samples": 5,
"ns_median": 118861317.0,
"ns_min": 117766708.0,
"ns_max": 119968958.0,
"bytes_median": 76348716.0,
"allocs_median": 1292863.0
},
"BenchmarkExtension/bilibili/moejs/warm": {
"samples": 5,
"ns_median": 3389193.0,
"ns_min": 3263242.0,
"ns_max": 3448471.0,
"bytes_median": 1305526.0,
"allocs_median": 11007.0
},
"BenchmarkExtension/bilibili/moejs/cold": {
"samples": 5,
"ns_median": 104827183.0,
"ns_min": 104022715.0,
"ns_max": 106050611.0,
"bytes_median": 62157413.0,
"allocs_median": 752967.0
},
"BenchmarkExtension/youtube/goja/warm": {
"samples": 5,
"ns_median": 3193004.0,
"ns_min": 3146585.0,
"ns_max": 3260717.0,
"bytes_median": 1201206.0,
"allocs_median": 25233.0
},
"BenchmarkExtension/youtube/goja/cold": {
"samples": 5,
"ns_median": 283412271.0,
"ns_min": 283191208.0,
"ns_max": 297033125.0,
"bytes_median": 164280012.0,
"allocs_median": 4518344.0
},
"BenchmarkExtension/youtube/moejs/warm": {
"samples": 5,
"ns_median": 276599.0,
"ns_min": 269731.0,
"ns_max": 286438.0,
"bytes_median": 153548.0,
"allocs_median": 1289.0
},
"BenchmarkExtension/youtube/moejs/cold": {
"samples": 5,
"ns_median": 239646417.0,
"ns_min": 238944375.0,
"ns_max": 253687520.0,
"bytes_median": 150780424.0,
"allocs_median": 1628650.0
},
"BenchmarkExtension/twitter/goja/warm": {
"samples": 5,
"ns_median": 3574601.0,
"ns_min": 3518603.0,
"ns_max": 3591739.0,
"bytes_median": 1682900.0,
"allocs_median": 32486.0
},
"BenchmarkExtension/twitter/goja/cold": {
"samples": 5,
"ns_median": 17238709.0,
"ns_min": 16791614.0,
"ns_max": 17428731.0,
"bytes_median": 11428458.0,
"allocs_median": 195739.0
},
"BenchmarkExtension/twitter/moejs/warm": {
"samples": 5,
"ns_median": 505147.0,
"ns_min": 497912.0,
"ns_max": 507563.0,
"bytes_median": 253665.0,
"allocs_median": 2799.0
},
"BenchmarkExtension/twitter/moejs/cold": {
"samples": 5,
"ns_median": 11849345.0,
"ns_min": 11735946.0,
"ns_max": 11875913.0,
"bytes_median": 8040679.0,
"allocs_median": 89924.0
},
"BenchmarkBufferedTransport/bilibili/goja": {
"samples": 5,
"ns_median": 22076250.0,
"ns_min": 20500819.0,
"ns_max": 22754704.0,
"bytes_median": 8228846.0,
"allocs_median": 141672.0
},
"BenchmarkBufferedTransport/bilibili/moejs": {
"samples": 5,
"ns_median": 3528287.0,
"ns_min": 3421821.0,
"ns_max": 3572089.0,
"bytes_median": 1297568.0,
"allocs_median": 10838.0
},
"BenchmarkBufferedTransport/youtube/goja": {
"samples": 5,
"ns_median": 2895027.0,
"ns_min": 2554084.0,
"ns_max": 2944065.0,
"bytes_median": 1096766.0,
"allocs_median": 22358.0
},
"BenchmarkBufferedTransport/youtube/moejs": {
"samples": 5,
"ns_median": 289619.0,
"ns_min": 274739.0,
"ns_max": 290746.0,
"bytes_median": 154919.0,
"allocs_median": 1299.0
},
"BenchmarkBufferedTransport/twitter/goja": {
"samples": 5,
"ns_median": 3450359.0,
"ns_min": 3359477.0,
"ns_max": 3460980.0,
"bytes_median": 1564097.0,
"allocs_median": 30491.0
},
"BenchmarkBufferedTransport/twitter/moejs": {
"samples": 5,
"ns_median": 491075.0,
"ns_min": 480354.0,
"ns_max": 495429.0,
"bytes_median": 243989.0,
"allocs_median": 2633.0
},
"BenchmarkBareRuntime/goja": {
"samples": 5,
"ns_median": 2847.0,
"ns_min": 2823.0,
"ns_max": 2893.0,
"bytes_median": 5216.0,
"allocs_median": 50.0
},
"BenchmarkBareRuntime/moejs-mutable": {
"samples": 5,
"ns_median": 48417.0,
"ns_min": 48202.0,
"ns_max": 50930.0,
"bytes_median": 106053.0,
"allocs_median": 145.0
},
"BenchmarkBareRuntime/moejs-shared": {
"samples": 5,
"ns_median": 3857.0,
"ns_min": 3839.0,
"ns_max": 3874.0,
"bytes_median": 5307.0,
"allocs_median": 42.0
}
}
-111
View File
@@ -1,111 +0,0 @@
goos: darwin
goarch: arm64
pkg: github.com/GopeedLab/gopeed/experiments/moejs
cpu: Apple M1
BenchmarkExtension/bilibili/goja/warm 39 19941715 ns/op 8340738 B/op 143388 allocs/op
BenchmarkExtension/bilibili/goja/warm 40 21722030 ns/op 8340729 B/op 143393 allocs/op
BenchmarkExtension/bilibili/goja/warm 38 21124144 ns/op 8340791 B/op 143401 allocs/op
BenchmarkExtension/bilibili/goja/warm 38 21188099 ns/op 8340744 B/op 143394 allocs/op
BenchmarkExtension/bilibili/goja/warm 39 21522829 ns/op 8340776 B/op 143399 allocs/op
BenchmarkExtension/bilibili/goja/cold 5 117766708 ns/op 76348771 B/op 1292869 allocs/op
BenchmarkExtension/bilibili/goja/cold 5 119968958 ns/op 76348876 B/op 1292861 allocs/op
BenchmarkExtension/bilibili/goja/cold 5 117841117 ns/op 76348716 B/op 1292863 allocs/op
BenchmarkExtension/bilibili/goja/cold 5 118861317 ns/op 76341414 B/op 1292867 allocs/op
BenchmarkExtension/bilibili/goja/cold 5 119065983 ns/op 76341294 B/op 1292854 allocs/op
BenchmarkExtension/bilibili/moejs/warm 180 3263242 ns/op 1304595 B/op 10995 allocs/op
BenchmarkExtension/bilibili/moejs/warm 180 3375801 ns/op 1305709 B/op 11011 allocs/op
BenchmarkExtension/bilibili/moejs/warm 182 3389193 ns/op 1305065 B/op 11002 allocs/op
BenchmarkExtension/bilibili/moejs/warm 180 3448471 ns/op 1305526 B/op 11007 allocs/op
BenchmarkExtension/bilibili/moejs/warm 171 3436332 ns/op 1305533 B/op 11008 allocs/op
BenchmarkExtension/bilibili/moejs/cold 5 104827183 ns/op 62224300 B/op 754651 allocs/op
BenchmarkExtension/bilibili/moejs/cold 6 104631410 ns/op 62138065 B/op 752864 allocs/op
BenchmarkExtension/bilibili/moejs/cold 6 106050611 ns/op 62145738 B/op 752980 allocs/op
BenchmarkExtension/bilibili/moejs/cold 6 104022715 ns/op 62157413 B/op 752954 allocs/op
BenchmarkExtension/bilibili/moejs/cold 6 105753188 ns/op 62159202 B/op 752967 allocs/op
BenchmarkExtension/youtube/goja/warm 195 3146585 ns/op 1201213 B/op 25233 allocs/op
BenchmarkExtension/youtube/goja/warm 159 3174966 ns/op 1201205 B/op 25233 allocs/op
BenchmarkExtension/youtube/goja/warm 163 3260717 ns/op 1201209 B/op 25233 allocs/op
BenchmarkExtension/youtube/goja/warm 165 3202571 ns/op 1201205 B/op 25233 allocs/op
BenchmarkExtension/youtube/goja/warm 158 3193004 ns/op 1201206 B/op 25233 allocs/op
BenchmarkExtension/youtube/goja/cold 2 285443125 ns/op 164279960 B/op 4518343 allocs/op
BenchmarkExtension/youtube/goja/cold 2 297033125 ns/op 164279944 B/op 4518343 allocs/op
BenchmarkExtension/youtube/goja/cold 2 283191208 ns/op 164280164 B/op 4518344 allocs/op
BenchmarkExtension/youtube/goja/cold 2 283412271 ns/op 164280012 B/op 4518344 allocs/op
BenchmarkExtension/youtube/goja/cold 2 283388230 ns/op 164280452 B/op 4518345 allocs/op
BenchmarkExtension/youtube/moejs/warm 2664 269731 ns/op 153550 B/op 1289 allocs/op
BenchmarkExtension/youtube/moejs/warm 2636 286438 ns/op 153550 B/op 1289 allocs/op
BenchmarkExtension/youtube/moejs/warm 2654 277087 ns/op 153548 B/op 1289 allocs/op
BenchmarkExtension/youtube/moejs/warm 2653 276165 ns/op 153545 B/op 1289 allocs/op
BenchmarkExtension/youtube/moejs/warm 2638 276599 ns/op 153547 B/op 1289 allocs/op
BenchmarkExtension/youtube/moejs/cold 3 238944375 ns/op 150707533 B/op 1628515 allocs/op
BenchmarkExtension/youtube/moejs/cold 3 243268042 ns/op 150800744 B/op 1628681 allocs/op
BenchmarkExtension/youtube/moejs/cold 3 239646417 ns/op 150709949 B/op 1628458 allocs/op
BenchmarkExtension/youtube/moejs/cold 2 253687520 ns/op 150780424 B/op 1631728 allocs/op
BenchmarkExtension/youtube/moejs/cold 3 239059042 ns/op 150786264 B/op 1628650 allocs/op
BenchmarkExtension/twitter/goja/warm 163 3518603 ns/op 1682901 B/op 32486 allocs/op
BenchmarkExtension/twitter/goja/warm 165 3570482 ns/op 1682898 B/op 32486 allocs/op
BenchmarkExtension/twitter/goja/warm 169 3575004 ns/op 1682900 B/op 32486 allocs/op
BenchmarkExtension/twitter/goja/warm 169 3574601 ns/op 1682900 B/op 32486 allocs/op
BenchmarkExtension/twitter/goja/warm 170 3591739 ns/op 1682900 B/op 32486 allocs/op
BenchmarkExtension/twitter/goja/cold 45 17377419 ns/op 11428470 B/op 195739 allocs/op
BenchmarkExtension/twitter/goja/cold 45 17238709 ns/op 11428451 B/op 195739 allocs/op
BenchmarkExtension/twitter/goja/cold 45 16791614 ns/op 11428452 B/op 195739 allocs/op
BenchmarkExtension/twitter/goja/cold 45 17428731 ns/op 11428460 B/op 195739 allocs/op
BenchmarkExtension/twitter/goja/cold 45 16943216 ns/op 11428458 B/op 195739 allocs/op
BenchmarkExtension/twitter/moejs/warm 1179 497912 ns/op 253663 B/op 2799 allocs/op
BenchmarkExtension/twitter/moejs/warm 1136 506676 ns/op 253669 B/op 2799 allocs/op
BenchmarkExtension/twitter/moejs/warm 1129 507563 ns/op 253674 B/op 2799 allocs/op
BenchmarkExtension/twitter/moejs/warm 1161 501615 ns/op 253665 B/op 2799 allocs/op
BenchmarkExtension/twitter/moejs/warm 1166 505147 ns/op 253663 B/op 2799 allocs/op
BenchmarkExtension/twitter/moejs/cold 61 11869347 ns/op 8040641 B/op 89924 allocs/op
BenchmarkExtension/twitter/moejs/cold 61 11735946 ns/op 8041001 B/op 89926 allocs/op
BenchmarkExtension/twitter/moejs/cold 62 11751223 ns/op 8040679 B/op 89923 allocs/op
BenchmarkExtension/twitter/moejs/cold 63 11849345 ns/op 8040427 B/op 89915 allocs/op
BenchmarkExtension/twitter/moejs/cold 63 11875913 ns/op 8040909 B/op 89925 allocs/op
BenchmarkBufferedTransport/bilibili/goja 40 20500819 ns/op 8228883 B/op 141673 allocs/op
BenchmarkBufferedTransport/bilibili/goja 39 22754704 ns/op 8228914 B/op 141680 allocs/op
BenchmarkBufferedTransport/bilibili/goja 26 21679981 ns/op 8228819 B/op 141668 allocs/op
BenchmarkBufferedTransport/bilibili/goja 40 22076250 ns/op 8228846 B/op 141672 allocs/op
BenchmarkBufferedTransport/bilibili/goja 39 22079190 ns/op 8228820 B/op 141668 allocs/op
BenchmarkBufferedTransport/bilibili/moejs 171 3446011 ns/op 1298601 B/op 10854 allocs/op
BenchmarkBufferedTransport/bilibili/moejs 176 3528287 ns/op 1297426 B/op 10837 allocs/op
BenchmarkBufferedTransport/bilibili/moejs 177 3560739 ns/op 1297948 B/op 10844 allocs/op
BenchmarkBufferedTransport/bilibili/moejs 177 3421821 ns/op 1297568 B/op 10838 allocs/op
BenchmarkBufferedTransport/bilibili/moejs 177 3572089 ns/op 1297237 B/op 10834 allocs/op
BenchmarkBufferedTransport/youtube/goja 223 2554084 ns/op 1096767 B/op 22358 allocs/op
BenchmarkBufferedTransport/youtube/goja 211 2895027 ns/op 1096766 B/op 22358 allocs/op
BenchmarkBufferedTransport/youtube/goja 212 2936390 ns/op 1096766 B/op 22358 allocs/op
BenchmarkBufferedTransport/youtube/goja 211 2944065 ns/op 1096766 B/op 22358 allocs/op
BenchmarkBufferedTransport/youtube/goja 222 2889798 ns/op 1096766 B/op 22358 allocs/op
BenchmarkBufferedTransport/youtube/moejs 2630 274739 ns/op 154922 B/op 1299 allocs/op
BenchmarkBufferedTransport/youtube/moejs 2602 289619 ns/op 154919 B/op 1299 allocs/op
BenchmarkBufferedTransport/youtube/moejs 2600 290746 ns/op 154920 B/op 1299 allocs/op
BenchmarkBufferedTransport/youtube/moejs 2618 290116 ns/op 154919 B/op 1299 allocs/op
BenchmarkBufferedTransport/youtube/moejs 2569 280026 ns/op 154914 B/op 1299 allocs/op
BenchmarkBufferedTransport/twitter/goja 172 3359477 ns/op 1564100 B/op 30491 allocs/op
BenchmarkBufferedTransport/twitter/goja 172 3450359 ns/op 1564097 B/op 30491 allocs/op
BenchmarkBufferedTransport/twitter/goja 170 3458607 ns/op 1564097 B/op 30491 allocs/op
BenchmarkBufferedTransport/twitter/goja 170 3401113 ns/op 1564097 B/op 30491 allocs/op
BenchmarkBufferedTransport/twitter/goja 170 3460980 ns/op 1564097 B/op 30491 allocs/op
BenchmarkBufferedTransport/twitter/moejs 1196 480354 ns/op 243983 B/op 2633 allocs/op
BenchmarkBufferedTransport/twitter/moejs 1146 490821 ns/op 243989 B/op 2633 allocs/op
BenchmarkBufferedTransport/twitter/moejs 1152 493628 ns/op 243989 B/op 2633 allocs/op
BenchmarkBufferedTransport/twitter/moejs 1144 491075 ns/op 243991 B/op 2633 allocs/op
BenchmarkBufferedTransport/twitter/moejs 1152 495429 ns/op 243989 B/op 2633 allocs/op
BenchmarkBareRuntime/goja 204234 2825 ns/op 5216 B/op 50 allocs/op
BenchmarkBareRuntime/goja 190192 2893 ns/op 5216 B/op 50 allocs/op
BenchmarkBareRuntime/goja 189585 2847 ns/op 5216 B/op 50 allocs/op
BenchmarkBareRuntime/goja 191311 2823 ns/op 5216 B/op 50 allocs/op
BenchmarkBareRuntime/goja 192636 2849 ns/op 5216 B/op 50 allocs/op
BenchmarkBareRuntime/moejs-mutable 10000 50930 ns/op 106053 B/op 145 allocs/op
BenchmarkBareRuntime/moejs-mutable 12385 48203 ns/op 106051 B/op 145 allocs/op
BenchmarkBareRuntime/moejs-mutable 12442 48417 ns/op 106053 B/op 145 allocs/op
BenchmarkBareRuntime/moejs-mutable 12380 48202 ns/op 106052 B/op 145 allocs/op
BenchmarkBareRuntime/moejs-mutable 12277 48647 ns/op 106053 B/op 145 allocs/op
BenchmarkBareRuntime/moejs-shared 181504 3863 ns/op 5307 B/op 42 allocs/op
BenchmarkBareRuntime/moejs-shared 179941 3851 ns/op 5307 B/op 42 allocs/op
BenchmarkBareRuntime/moejs-shared 181015 3839 ns/op 5307 B/op 42 allocs/op
BenchmarkBareRuntime/moejs-shared 183372 3857 ns/op 5307 B/op 42 allocs/op
BenchmarkBareRuntime/moejs-shared 178731 3874 ns/op 5307 B/op 42 allocs/op
PASS
ok github.com/GopeedLab/gopeed/experiments/moejs 118.047s
-112
View File
@@ -1,112 +0,0 @@
=== RUN TestMoeResolveWriteback
bridge_test.go:44: Go Resource.Validate passed, name=BV1BJ4m1e7g8.真实扩展 Replay 测试(P2).1080p.mp4
--- PASS: TestMoeResolveWriteback (0.09s)
=== RUN TestBufferedReplay
=== RUN TestBufferedReplay/bilibili
=== RUN TestBufferedReplay/bilibili/goja
=== RUN TestBufferedReplay/bilibili/moejs
=== RUN TestBufferedReplay/youtube
=== RUN TestBufferedReplay/youtube/goja
=== RUN TestBufferedReplay/youtube/moejs
=== RUN TestBufferedReplay/twitter
=== RUN TestBufferedReplay/twitter/goja
=== RUN TestBufferedReplay/twitter/moejs
--- PASS: TestBufferedReplay (0.60s)
--- PASS: TestBufferedReplay/bilibili (0.18s)
--- PASS: TestBufferedReplay/bilibili/goja (0.09s)
--- PASS: TestBufferedReplay/bilibili/moejs (0.09s)
--- PASS: TestBufferedReplay/youtube (0.40s)
--- PASS: TestBufferedReplay/youtube/goja (0.21s)
--- PASS: TestBufferedReplay/youtube/moejs (0.19s)
--- PASS: TestBufferedReplay/twitter (0.02s)
--- PASS: TestBufferedReplay/twitter/goja (0.01s)
--- PASS: TestBufferedReplay/twitter/moejs (0.01s)
=== RUN TestExtensionReplay
=== RUN TestExtensionReplay/bilibili
=== RUN TestExtensionReplay/bilibili/goja
evaluation_test.go:46: {"res":{"name":"真实扩展 Replay 测试","range":false,"files":[{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P1).1080p.mp4","req":{"url":"blob:replay/1","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":100,"p":0,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p"}}},{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P2).1080p.mp4","req":{"url":"blob:replay/2","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":101,"p":1,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p"}}},{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P3).1080p.mp4","req":{"url":"blob:replay/3","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":102,"p":2,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p"}}}]},"requests":["/x/web-interface/view","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl"]}
=== RUN TestExtensionReplay/bilibili/moejs
evaluation_test.go:46: {"res":{"name":"真实扩展 Replay 测试","range":false,"files":[{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P1).1080p.mp4","req":{"url":"blob:replay/1","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":100,"p":0,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p"}}},{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P2).1080p.mp4","req":{"url":"blob:replay/2","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":101,"p":1,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p"}}},{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P3).1080p.mp4","req":{"url":"blob:replay/3","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":102,"p":2,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p"}}}]},"requests":["/x/web-interface/view","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl"]}
=== RUN TestExtensionReplay/youtube
=== RUN TestExtensionReplay/youtube/goja
evaluation_test.go:46: {"res":{"range":false,"files":[{"name":"YouTube Replay 测试.mp4","req":{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","rawUrl":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","labels":{"monkeyWie@youtube":"1","input":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","quality":"1080p","fallbackToBest":"true","type":"merged"}}}]},"requests":["/sw.js_data","/youtubei/v1/config","/youtubei/v1/player"]}
=== RUN TestExtensionReplay/youtube/moejs
evaluation_test.go:46: {"res":{"range":false,"files":[{"name":"YouTube Replay 测试.mp4","req":{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","rawUrl":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","labels":{"monkeyWie@youtube":"1","input":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","quality":"1080p","fallbackToBest":"true","type":"merged"}}}]},"requests":["/sw.js_data","/youtubei/v1/config","/youtubei/v1/player"]}
=== RUN TestExtensionReplay/twitter
=== RUN TestExtensionReplay/twitter/goja
evaluation_test.go:46: {"res":{"name":"twitter-status-1734145541599383880","files":[{"name":"twitter-status-1734145541599383880-1.jpg","req":{"url":"https://pbs.twimg.com/media/replay.jpg"}},{"name":"twitter-status-1734145541599383880-2.mp4","req":{"url":"https://video.twimg.com/1920x1080/high.mp4"}}]},"requests":["/raw/nz3ApKQM","/1.1/guest/activate.json","/raw/nz3ApKQM","/i/api/graphql/DJS3BdhUhcaEpZ7B7irJDg/TweetResultByRestId"]}
=== RUN TestExtensionReplay/twitter/moejs
evaluation_test.go:46: {"res":{"name":"twitter-status-1734145541599383880","files":[{"name":"twitter-status-1734145541599383880-1.jpg","req":{"url":"https://pbs.twimg.com/media/replay.jpg"}},{"name":"twitter-status-1734145541599383880-2.mp4","req":{"url":"https://video.twimg.com/1920x1080/high.mp4"}}]},"requests":["/raw/nz3ApKQM","/1.1/guest/activate.json","/raw/nz3ApKQM","/i/api/graphql/DJS3BdhUhcaEpZ7B7irJDg/TweetResultByRestId"]}
--- PASS: TestExtensionReplay (0.59s)
--- PASS: TestExtensionReplay/bilibili (0.17s)
--- PASS: TestExtensionReplay/bilibili/goja (0.09s)
--- PASS: TestExtensionReplay/bilibili/moejs (0.09s)
--- PASS: TestExtensionReplay/youtube (0.39s)
--- PASS: TestExtensionReplay/youtube/goja (0.21s)
--- PASS: TestExtensionReplay/youtube/moejs (0.19s)
--- PASS: TestExtensionReplay/twitter (0.02s)
--- PASS: TestExtensionReplay/twitter/goja (0.01s)
--- PASS: TestExtensionReplay/twitter/moejs (0.01s)
=== RUN TestExtensionLifecycle
=== RUN TestExtensionLifecycle/bilibili
=== RUN TestExtensionLifecycle/bilibili/goja
evaluation_test.go:110: {"res":{"name":"真实扩展 Replay 测试","range":false,"files":[{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P1).1080p.mp4","req":{"url":"blob:replay/5","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":100,"p":0,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p","mergeCdnState":"{\"failed\":[{\"video\":\"https://upos-test.bilivideo.com\",\"audio\":\"https://upos-test.bilivideo.com\"}],\"retries\":1,\"current\":{\"video\":\"https://backup.example.com\",\"audio\":\"https://upos-test.bilivideo.com\"}}"}}},{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P2).1080p.mp4","req":{"url":"blob:replay/7","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":101,"p":1,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p","mergeCdnState":"{\"failed\":[{\"video\":\"https://upos-test.bilivideo.com\",\"audio\":\"https://upos-test.bilivideo.com\"}],\"retries\":1,\"current\":{\"video\":\"https://backup.example.com\",\"audio\":\"https://upos-test.bilivideo.com\"}}"}}},{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P3).1080p.mp4","req":{"url":"blob:replay/9","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":102,"p":2,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p","mergeCdnState":"{\"failed\":[{\"video\":\"https://upos-test.bilivideo.com\",\"audio\":\"https://upos-test.bilivideo.com\"}],\"retries\":1,\"current\":{\"video\":\"https://backup.example.com\",\"audio\":\"https://upos-test.bilivideo.com\"}}"}}}]},"continued":3,"requests":["/x/web-interface/view","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl"]}
=== RUN TestExtensionLifecycle/bilibili/moejs
evaluation_test.go:110: {"res":{"name":"真实扩展 Replay 测试","range":false,"files":[{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P1).1080p.mp4","req":{"url":"blob:replay/5","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":100,"p":0,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p","mergeCdnState":"{\"failed\":[{\"video\":\"https://upos-test.bilivideo.com\",\"audio\":\"https://upos-test.bilivideo.com\"}],\"retries\":1,\"current\":{\"video\":\"https://backup.example.com\",\"audio\":\"https://upos-test.bilivideo.com\"}}"}}},{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P2).1080p.mp4","req":{"url":"blob:replay/7","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":101,"p":1,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p","mergeCdnState":"{\"failed\":[{\"video\":\"https://upos-test.bilivideo.com\",\"audio\":\"https://upos-test.bilivideo.com\"}],\"retries\":1,\"current\":{\"video\":\"https://backup.example.com\",\"audio\":\"https://upos-test.bilivideo.com\"}}"}}},{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P3).1080p.mp4","req":{"url":"blob:replay/9","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":102,"p":2,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p","mergeCdnState":"{\"failed\":[{\"video\":\"https://upos-test.bilivideo.com\",\"audio\":\"https://upos-test.bilivideo.com\"}],\"retries\":1,\"current\":{\"video\":\"https://backup.example.com\",\"audio\":\"https://upos-test.bilivideo.com\"}}"}}}]},"continued":3,"requests":["/x/web-interface/view","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl"]}
=== RUN TestExtensionLifecycle/youtube
=== RUN TestExtensionLifecycle/youtube/goja
evaluation_test.go:110: {"res":{"range":false,"files":[{"name":"YouTube Replay 测试.mp4","req":{"url":"blob:replay/1","rawUrl":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","labels":{"monkeyWie@youtube":"1","input":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","quality":"1080p","fallbackToBest":"true","type":"merged","retried":"1"}}}]},"continued":1,"requests":["/sw.js_data","/youtubei/v1/config","/youtubei/v1/player"]}
=== RUN TestExtensionLifecycle/youtube/moejs
evaluation_test.go:110: {"res":{"range":false,"files":[{"name":"YouTube Replay 测试.mp4","req":{"url":"blob:replay/1","rawUrl":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","labels":{"monkeyWie@youtube":"1","input":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","quality":"1080p","fallbackToBest":"true","type":"merged","retried":"1"}}}]},"continued":1,"requests":["/sw.js_data","/youtubei/v1/config","/youtubei/v1/player"]}
=== RUN TestExtensionLifecycle/twitter
=== RUN TestExtensionLifecycle/twitter/goja
evaluation_test.go:110: {"res":{"name":"twitter-status-1734145541599383880","files":[{"name":"twitter-status-1734145541599383880-1.jpg","req":{"url":"https://pbs.twimg.com/media/replay.jpg"}},{"name":"twitter-status-1734145541599383880-2.mp4","req":{"url":"https://video.twimg.com/1920x1080/high.mp4"}}]},"continued":0,"requests":["/raw/nz3ApKQM","/1.1/guest/activate.json","/raw/nz3ApKQM","/i/api/graphql/DJS3BdhUhcaEpZ7B7irJDg/TweetResultByRestId"]}
=== RUN TestExtensionLifecycle/twitter/moejs
evaluation_test.go:110: {"res":{"name":"twitter-status-1734145541599383880","files":[{"name":"twitter-status-1734145541599383880-1.jpg","req":{"url":"https://pbs.twimg.com/media/replay.jpg"}},{"name":"twitter-status-1734145541599383880-2.mp4","req":{"url":"https://video.twimg.com/1920x1080/high.mp4"}}]},"continued":0,"requests":["/raw/nz3ApKQM","/1.1/guest/activate.json","/raw/nz3ApKQM","/i/api/graphql/DJS3BdhUhcaEpZ7B7irJDg/TweetResultByRestId"]}
--- PASS: TestExtensionLifecycle (0.63s)
--- PASS: TestExtensionLifecycle/bilibili (0.20s)
--- PASS: TestExtensionLifecycle/bilibili/goja (0.11s)
--- PASS: TestExtensionLifecycle/bilibili/moejs (0.09s)
--- PASS: TestExtensionLifecycle/youtube (0.40s)
--- PASS: TestExtensionLifecycle/youtube/goja (0.21s)
--- PASS: TestExtensionLifecycle/youtube/moejs (0.19s)
--- PASS: TestExtensionLifecycle/twitter (0.02s)
--- PASS: TestExtensionLifecycle/twitter/goja (0.01s)
--- PASS: TestExtensionLifecycle/twitter/moejs (0.01s)
=== RUN TestFrozenExtensionReplay
=== RUN TestFrozenExtensionReplay/bilibili
frozen_test.go:13: {"res":{"name":"真实扩展 Replay 测试","range":false,"files":[{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P1).1080p.mp4","req":{"url":"blob:replay/1","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":100,"p":0,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p"}}},{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P2).1080p.mp4","req":{"url":"blob:replay/2","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":101,"p":1,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p"}}},{"name":"BV1BJ4m1e7g8.真实扩展 Replay 测试(P3).1080p.mp4","req":{"url":"blob:replay/3","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":102,"p":2,"type":"merged","quality":80,"selectedQuality":80,"resolution":"1080p"}}}]},"requests":["/x/web-interface/view","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl","/x/web-interface/nav","/x/player/wbi/playurl"]}
=== RUN TestFrozenExtensionReplay/youtube
frozen_test.go:13: {"res":{"range":false,"files":[{"name":"YouTube Replay 测试.mp4","req":{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","rawUrl":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","labels":{"monkeyWie@youtube":"1","input":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","quality":"1080p","fallbackToBest":"true","type":"merged"}}}]},"requests":["/sw.js_data","/youtubei/v1/config","/youtubei/v1/player"]}
=== RUN TestFrozenExtensionReplay/twitter
frozen_test.go:13: {"res":{"name":"twitter-status-1734145541599383880","files":[{"name":"twitter-status-1734145541599383880-1.jpg","req":{"url":"https://pbs.twimg.com/media/replay.jpg"}},{"name":"twitter-status-1734145541599383880-2.mp4","req":{"url":"https://video.twimg.com/1920x1080/high.mp4"}}]},"requests":["/raw/nz3ApKQM","/1.1/guest/activate.json","/raw/nz3ApKQM","/i/api/graphql/DJS3BdhUhcaEpZ7B7irJDg/TweetResultByRestId"]}
--- PASS: TestFrozenExtensionReplay (0.28s)
--- PASS: TestFrozenExtensionReplay/bilibili (0.08s)
--- PASS: TestFrozenExtensionReplay/youtube (0.19s)
--- PASS: TestFrozenExtensionReplay/twitter (0.01s)
=== RUN TestAsyncHTTP
=== RUN TestAsyncHTTP/goja
http_test.go:46: abort rejection: ReferenceError: DOMException is not defined
http_test.go:56: default abort probe: AbortError
=== RUN TestAsyncHTTP/moejs
http_test.go:46: abort rejection: Error: evaluation cancel
http_test.go:56: default abort probe: AbortError
--- PASS: TestAsyncHTTP (0.04s)
--- PASS: TestAsyncHTTP/goja (0.02s)
--- PASS: TestAsyncHTTP/moejs (0.02s)
=== RUN TestHostInterop
=== RUN TestHostInterop/goja
interop_test.go:65: Go URL=after, Go res=map[files:[map[name:video.mp4]]], JS method=function
=== RUN TestHostInterop/moejs
interop_test.go:65: Go URL=before, Go res=map[], JS method=undefined
--- PASS: TestHostInterop (0.00s)
--- PASS: TestHostInterop/goja (0.00s)
--- PASS: TestHostInterop/moejs (0.00s)
=== RUN TestFrozenCompatibility
interop_test.go:80: TypeError: Cannot modify property '__probe' of shared intrinsic [object Array]
--- PASS: TestFrozenCompatibility (0.00s)
=== RUN TestLiveResolve
live_test.go:14: set GOPEED_MOEJS_LIVE=1 to contact public endpoints
--- SKIP: TestLiveResolve (0.00s)
PASS
ok github.com/GopeedLab/gopeed/experiments/moejs 2.824s
-12
View File
@@ -1,12 +0,0 @@
=== RUN TestAsyncHTTP
=== RUN TestAsyncHTTP/goja
http_test.go:46: abort rejection: ReferenceError: DOMException is not defined
http_test.go:56: default abort probe: AbortError
=== RUN TestAsyncHTTP/moejs
http_test.go:46: abort rejection: Error: evaluation cancel
http_test.go:56: default abort probe: AbortError
--- PASS: TestAsyncHTTP (0.04s)
--- PASS: TestAsyncHTTP/goja (0.02s)
--- PASS: TestAsyncHTTP/moejs (0.02s)
PASS
ok github.com/GopeedLab/gopeed/experiments/moejs 0.631s
-36
View File
@@ -1,36 +0,0 @@
=== RUN TestAsyncHTTP
=== RUN TestAsyncHTTP/goja
http_test.go:35: abort rejection: ReferenceError: DOMException is not defined
=== RUN TestAsyncHTTP/moejs
http_test.go:35: abort rejection: AbortError: This operation was aborted
--- PASS: TestAsyncHTTP (0.04s)
--- PASS: TestAsyncHTTP/goja (0.02s)
--- PASS: TestAsyncHTTP/moejs (0.02s)
=== RUN TestLiveResolve
=== RUN TestLiveResolve/bilibili/goja
live_test.go:36: elapsed=1.205312291s
live_test.go:45: {"res":{"name":"李宏毅2024春《生成式人工智能导论》","range":false,"files":[{"name":"BV1BJ4m1e7g8.李宏毅2024春《生成式人工智能导论》(P1).480p.mp4","req":{"url":"blob:replay/1","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":1449518003,"p":0,"type":"merged","quality":80,"selectedQuality":32,"resolution":"480p"}}},{"name":"BV1BJ4m1e7g8.李宏毅2024春《生成式人工智能导论》(P2).480p.mp4","req":{"url":"blob:replay/2","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":1449532181,"p":1,"type":"merged","quality":80,"selectedQuality":32,"resolution":"480p"}}},{"name":"BV1BJ4m1e7g8.李宏毅2024春《生成式人工智能导论》(P3).480p.mp4","req":{"url":"blob:replay/3","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":1456854415,"p":2,"type":"merged","quality":80,"selectedQuality":32,"resolution":"480p"}}}]},"requests":[]}
=== RUN TestLiveResolve/bilibili/moejs
live_test.go:36: elapsed=521.812875ms
live_test.go:45: {"res":{"name":"李宏毅2024春《生成式人工智能导论》","range":false,"files":[{"name":"BV1BJ4m1e7g8.李宏毅2024春《生成式人工智能导论》(P1).480p.mp4","req":{"url":"blob:replay/1","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":1449518003,"p":0,"type":"merged","quality":80,"selectedQuality":32,"resolution":"480p"}}},{"name":"BV1BJ4m1e7g8.李宏毅2024春《生成式人工智能导论》(P2).480p.mp4","req":{"url":"blob:replay/2","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":1449532181,"p":1,"type":"merged","quality":80,"selectedQuality":32,"resolution":"480p"}}},{"name":"BV1BJ4m1e7g8.李宏毅2024春《生成式人工智能导论》(P3).480p.mp4","req":{"url":"blob:replay/3","rawUrl":"https://www.bilibili.com/video/BV1BJ4m1e7g8?p=1-3","labels":{"monkeyWie@bilibili":"1","bvid":"BV1BJ4m1e7g8","cid":1456854415,"p":2,"type":"merged","quality":80,"selectedQuality":32,"resolution":"480p"}}}]},"requests":[]}
=== RUN TestLiveResolve/youtube/goja
live_test.go:36: elapsed=1.994355417s
live_test.go:45: {"res":{"range":false,"files":[{"name":"Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster).mp4","req":{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","rawUrl":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","labels":{"monkeyWie@youtube":"1","input":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","quality":"1080p","fallbackToBest":"true","type":"merged"}}}]},"requests":[]}
=== RUN TestLiveResolve/youtube/moejs
live_test.go:36: elapsed=969.283625ms
live_test.go:45: {"res":{"range":false,"files":[{"name":"Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster).mp4","req":{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","rawUrl":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","labels":{"monkeyWie@youtube":"1","input":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","quality":"1080p","fallbackToBest":"true","type":"merged"}}}]},"requests":[]}
=== RUN TestLiveResolve/twitter/goja
live_test.go:36: elapsed=2.249496s
live_test.go:45: {"res":{"name":"twitter-status-1734145541599383880","files":[{"name":"twitter-status-1734145541599383880-1.mp4","req":{"url":"https://video.twimg.com/ext_tw_video/1734143688874561536/pu/vid/avc1/1280x720/hpx1Z8GQYa0FVgGb.mp4?tag=12"}}]},"requests":[]}
=== RUN TestLiveResolve/twitter/moejs
live_test.go:36: elapsed=2.231541167s
live_test.go:45: {"res":{"name":"twitter-status-1734145541599383880","files":[{"name":"twitter-status-1734145541599383880-1.mp4","req":{"url":"https://video.twimg.com/ext_tw_video/1734143688874561536/pu/vid/avc1/1280x720/hpx1Z8GQYa0FVgGb.mp4?tag=12"}}]},"requests":[]}
--- PASS: TestLiveResolve (9.86s)
--- PASS: TestLiveResolve/bilibili/goja (1.28s)
--- PASS: TestLiveResolve/bilibili/moejs (0.61s)
--- PASS: TestLiveResolve/youtube/goja (2.24s)
--- PASS: TestLiveResolve/youtube/moejs (1.19s)
--- PASS: TestLiveResolve/twitter/goja (2.28s)
--- PASS: TestLiveResolve/twitter/moejs (2.26s)
PASS
ok github.com/GopeedLab/gopeed/experiments/moejs 10.511s
@@ -1,11 +0,0 @@
ok github.com/GopeedLab/gopeed/pkg/download/engine 15.886s
ok github.com/GopeedLab/gopeed/pkg/download/engine/ffmpeg 12.799s
? github.com/GopeedLab/gopeed/pkg/download/engine/inject/error [no test files]
? github.com/GopeedLab/gopeed/pkg/download/engine/inject/fetch [no test files]
? github.com/GopeedLab/gopeed/pkg/download/engine/inject/ffmpeg [no test files]
? github.com/GopeedLab/gopeed/pkg/download/engine/inject/file [no test files]
ok github.com/GopeedLab/gopeed/pkg/download/engine/inject/stream 2.423s
? github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm [no test files]
? github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr [no test files]
? github.com/GopeedLab/gopeed/pkg/download/engine/util [no test files]
ok github.com/GopeedLab/gopeed/pkg/download/engine/webview 0.777s
@@ -1,60 +0,0 @@
bilibili/retained/1/goja repeat=1: CPU=128.924 ms/op, peakRSS=113.7 MiB
bilibili/retained/1/moejs repeat=1: CPU=108.563 ms/op, peakRSS=109.7 MiB
bilibili/retained/1/moejs repeat=2: CPU=103.893 ms/op, peakRSS=110.5 MiB
bilibili/retained/1/goja repeat=2: CPU=119.400 ms/op, peakRSS=113.9 MiB
bilibili/retained/1/goja repeat=3: CPU=125.350 ms/op, peakRSS=117.8 MiB
bilibili/retained/1/moejs repeat=3: CPU=110.583 ms/op, peakRSS=110.8 MiB
bilibili/retained/1/moejs repeat=4: CPU=104.953 ms/op, peakRSS=110.7 MiB
bilibili/retained/1/goja repeat=4: CPU=129.226 ms/op, peakRSS=112.7 MiB
bilibili/retained/1/goja repeat=5: CPU=117.278 ms/op, peakRSS=113.5 MiB
bilibili/retained/1/moejs repeat=5: CPU=105.932 ms/op, peakRSS=110.6 MiB
bilibili/retained/8/goja repeat=1: CPU=135.193 ms/op, peakRSS=339.0 MiB
bilibili/retained/8/moejs repeat=1: CPU=106.505 ms/op, peakRSS=291.5 MiB
bilibili/retained/8/moejs repeat=2: CPU=108.446 ms/op, peakRSS=322.6 MiB
bilibili/retained/8/goja repeat=2: CPU=138.953 ms/op, peakRSS=361.2 MiB
bilibili/retained/8/goja repeat=3: CPU=137.391 ms/op, peakRSS=362.8 MiB
bilibili/retained/8/moejs repeat=3: CPU=106.384 ms/op, peakRSS=276.2 MiB
bilibili/retained/8/moejs repeat=4: CPU=106.178 ms/op, peakRSS=302.2 MiB
bilibili/retained/8/goja repeat=4: CPU=135.749 ms/op, peakRSS=363.1 MiB
bilibili/retained/8/goja repeat=5: CPU=136.385 ms/op, peakRSS=357.0 MiB
bilibili/retained/8/moejs repeat=5: CPU=106.298 ms/op, peakRSS=309.8 MiB
youtube/retained/1/goja repeat=1: CPU=263.248 ms/op, peakRSS=202.4 MiB
youtube/retained/1/moejs repeat=1: CPU=249.762 ms/op, peakRSS=189.8 MiB
youtube/retained/1/moejs repeat=2: CPU=239.557 ms/op, peakRSS=194.6 MiB
youtube/retained/1/goja repeat=2: CPU=258.828 ms/op, peakRSS=204.4 MiB
youtube/retained/1/goja repeat=3: CPU=262.150 ms/op, peakRSS=203.1 MiB
youtube/retained/1/moejs repeat=3: CPU=242.040 ms/op, peakRSS=191.3 MiB
youtube/retained/1/moejs repeat=4: CPU=237.677 ms/op, peakRSS=194.4 MiB
youtube/retained/1/goja repeat=4: CPU=260.255 ms/op, peakRSS=201.9 MiB
youtube/retained/1/goja repeat=5: CPU=275.425 ms/op, peakRSS=205.3 MiB
youtube/retained/1/moejs repeat=5: CPU=254.674 ms/op, peakRSS=190.8 MiB
youtube/retained/8/goja repeat=1: CPU=293.475 ms/op, peakRSS=663.5 MiB
youtube/retained/8/moejs repeat=1: CPU=233.453 ms/op, peakRSS=699.2 MiB
youtube/retained/8/moejs repeat=2: CPU=232.512 ms/op, peakRSS=679.2 MiB
youtube/retained/8/goja repeat=2: CPU=295.749 ms/op, peakRSS=668.1 MiB
youtube/retained/8/goja repeat=3: CPU=293.476 ms/op, peakRSS=607.0 MiB
youtube/retained/8/moejs repeat=3: CPU=231.929 ms/op, peakRSS=623.0 MiB
youtube/retained/8/moejs repeat=4: CPU=233.816 ms/op, peakRSS=698.8 MiB
youtube/retained/8/goja repeat=4: CPU=284.988 ms/op, peakRSS=616.5 MiB
youtube/retained/8/goja repeat=5: CPU=299.837 ms/op, peakRSS=633.7 MiB
youtube/retained/8/moejs repeat=5: CPU=232.017 ms/op, peakRSS=623.5 MiB
twitter/retained/1/goja repeat=1: CPU=14.898 ms/op, peakRSS=75.3 MiB
twitter/retained/1/moejs repeat=1: CPU=11.448 ms/op, peakRSS=74.1 MiB
twitter/retained/1/moejs repeat=2: CPU=11.200 ms/op, peakRSS=75.0 MiB
twitter/retained/1/goja repeat=2: CPU=15.070 ms/op, peakRSS=95.0 MiB
twitter/retained/1/goja repeat=3: CPU=15.161 ms/op, peakRSS=75.4 MiB
twitter/retained/1/moejs repeat=3: CPU=11.442 ms/op, peakRSS=74.5 MiB
twitter/retained/1/moejs repeat=4: CPU=11.417 ms/op, peakRSS=74.4 MiB
twitter/retained/1/goja repeat=4: CPU=15.046 ms/op, peakRSS=75.3 MiB
twitter/retained/1/goja repeat=5: CPU=15.185 ms/op, peakRSS=75.2 MiB
twitter/retained/1/moejs repeat=5: CPU=12.036 ms/op, peakRSS=74.2 MiB
twitter/retained/8/goja repeat=1: CPU=19.321 ms/op, peakRSS=106.7 MiB
twitter/retained/8/moejs repeat=1: CPU=12.334 ms/op, peakRSS=93.3 MiB
twitter/retained/8/moejs repeat=2: CPU=12.144 ms/op, peakRSS=93.3 MiB
twitter/retained/8/goja repeat=2: CPU=19.295 ms/op, peakRSS=109.0 MiB
twitter/retained/8/goja repeat=3: CPU=18.405 ms/op, peakRSS=98.2 MiB
twitter/retained/8/moejs repeat=3: CPU=11.997 ms/op, peakRSS=93.2 MiB
twitter/retained/8/moejs repeat=4: CPU=12.301 ms/op, peakRSS=93.2 MiB
twitter/retained/8/goja repeat=4: CPU=19.434 ms/op, peakRSS=106.7 MiB
twitter/retained/8/goja repeat=5: CPU=18.415 ms/op, peakRSS=98.4 MiB
twitter/retained/8/moejs repeat=5: CPU=12.492 ms/op, peakRSS=92.9 MiB
-120
View File
@@ -1,120 +0,0 @@
bilibili/cold/20/goja repeat=1: CPU=119.376 ms/op, peakRSS=154.7 MiB
bilibili/cold/20/moejs repeat=1: CPU=105.032 ms/op, peakRSS=171.3 MiB
bilibili/cold/20/moejs repeat=2: CPU=103.703 ms/op, peakRSS=178.6 MiB
bilibili/cold/20/goja repeat=2: CPU=118.962 ms/op, peakRSS=151.4 MiB
bilibili/cold/20/goja repeat=3: CPU=118.279 ms/op, peakRSS=143.4 MiB
bilibili/cold/20/moejs repeat=3: CPU=103.668 ms/op, peakRSS=172.3 MiB
bilibili/cold/20/moejs repeat=4: CPU=104.081 ms/op, peakRSS=174.6 MiB
bilibili/cold/20/goja repeat=4: CPU=116.987 ms/op, peakRSS=155.2 MiB
bilibili/cold/20/goja repeat=5: CPU=119.285 ms/op, peakRSS=158.0 MiB
bilibili/cold/20/moejs repeat=5: CPU=104.162 ms/op, peakRSS=167.5 MiB
bilibili/warm/200/goja repeat=1: CPU=19.869 ms/op, peakRSS=127.2 MiB
bilibili/warm/200/moejs repeat=1: CPU=3.173 ms/op, peakRSS=118.5 MiB
bilibili/warm/200/moejs repeat=2: CPU=3.166 ms/op, peakRSS=118.2 MiB
bilibili/warm/200/goja repeat=2: CPU=19.644 ms/op, peakRSS=124.0 MiB
bilibili/warm/200/goja repeat=3: CPU=19.543 ms/op, peakRSS=127.3 MiB
bilibili/warm/200/moejs repeat=3: CPU=3.171 ms/op, peakRSS=118.5 MiB
bilibili/warm/200/moejs repeat=4: CPU=3.202 ms/op, peakRSS=119.3 MiB
bilibili/warm/200/goja repeat=4: CPU=19.688 ms/op, peakRSS=124.5 MiB
bilibili/warm/200/goja repeat=5: CPU=19.852 ms/op, peakRSS=125.2 MiB
bilibili/warm/200/moejs repeat=5: CPU=3.208 ms/op, peakRSS=118.6 MiB
bilibili/retained/1/goja repeat=1: CPU=132.688 ms/op, peakRSS=121.9 MiB
bilibili/retained/1/moejs repeat=1: CPU=108.554 ms/op, peakRSS=109.5 MiB
bilibili/retained/1/moejs repeat=2: CPU=111.625 ms/op, peakRSS=110.8 MiB
bilibili/retained/1/goja repeat=2: CPU=134.311 ms/op, peakRSS=121.9 MiB
bilibili/retained/1/goja repeat=3: CPU=128.855 ms/op, peakRSS=114.1 MiB
bilibili/retained/1/moejs repeat=3: CPU=109.405 ms/op, peakRSS=109.4 MiB
bilibili/retained/1/moejs repeat=4: CPU=107.809 ms/op, peakRSS=110.5 MiB
bilibili/retained/1/goja repeat=4: CPU=118.308 ms/op, peakRSS=108.8 MiB
bilibili/retained/1/goja repeat=5: CPU=116.917 ms/op, peakRSS=108.7 MiB
bilibili/retained/1/moejs repeat=5: CPU=108.363 ms/op, peakRSS=108.9 MiB
bilibili/retained/8/goja repeat=1: CPU=140.169 ms/op, peakRSS=362.2 MiB
bilibili/retained/8/moejs repeat=1: CPU=107.816 ms/op, peakRSS=306.9 MiB
bilibili/retained/8/moejs repeat=2: CPU=105.929 ms/op, peakRSS=298.2 MiB
bilibili/retained/8/goja repeat=2: CPU=134.081 ms/op, peakRSS=329.5 MiB
bilibili/retained/8/goja repeat=3: CPU=133.975 ms/op, peakRSS=355.8 MiB
bilibili/retained/8/moejs repeat=3: CPU=107.769 ms/op, peakRSS=316.6 MiB
bilibili/retained/8/moejs repeat=4: CPU=105.431 ms/op, peakRSS=271.5 MiB
bilibili/retained/8/goja repeat=4: CPU=134.465 ms/op, peakRSS=346.5 MiB
bilibili/retained/8/goja repeat=5: CPU=142.183 ms/op, peakRSS=361.4 MiB
bilibili/retained/8/moejs repeat=5: CPU=107.668 ms/op, peakRSS=292.8 MiB
youtube/cold/20/goja repeat=1: CPU=280.323 ms/op, peakRSS=293.2 MiB
youtube/cold/20/moejs repeat=1: CPU=235.688 ms/op, peakRSS=339.6 MiB
youtube/cold/20/moejs repeat=2: CPU=239.981 ms/op, peakRSS=374.8 MiB
youtube/cold/20/goja repeat=2: CPU=278.476 ms/op, peakRSS=283.8 MiB
youtube/cold/20/goja repeat=3: CPU=269.321 ms/op, peakRSS=247.4 MiB
youtube/cold/20/moejs repeat=3: CPU=237.330 ms/op, peakRSS=353.6 MiB
youtube/cold/20/moejs repeat=4: CPU=239.973 ms/op, peakRSS=376.2 MiB
youtube/cold/20/goja repeat=4: CPU=279.683 ms/op, peakRSS=269.2 MiB
youtube/cold/20/goja repeat=5: CPU=275.033 ms/op, peakRSS=288.9 MiB
youtube/cold/20/moejs repeat=5: CPU=237.308 ms/op, peakRSS=358.9 MiB
youtube/warm/200/goja repeat=1: CPU=2.895 ms/op, peakRSS=205.4 MiB
youtube/warm/200/moejs repeat=1: CPU=0.215 ms/op, peakRSS=198.4 MiB
youtube/warm/200/moejs repeat=2: CPU=0.215 ms/op, peakRSS=193.8 MiB
youtube/warm/200/goja repeat=2: CPU=2.905 ms/op, peakRSS=203.9 MiB
youtube/warm/200/goja repeat=3: CPU=2.993 ms/op, peakRSS=204.5 MiB
youtube/warm/200/moejs repeat=3: CPU=0.216 ms/op, peakRSS=201.8 MiB
youtube/warm/200/moejs repeat=4: CPU=0.216 ms/op, peakRSS=199.2 MiB
youtube/warm/200/goja repeat=4: CPU=2.912 ms/op, peakRSS=204.5 MiB
youtube/warm/200/goja repeat=5: CPU=2.900 ms/op, peakRSS=205.4 MiB
youtube/warm/200/moejs repeat=5: CPU=0.214 ms/op, peakRSS=201.7 MiB
youtube/retained/1/goja repeat=1: CPU=266.054 ms/op, peakRSS=202.5 MiB
youtube/retained/1/moejs repeat=1: CPU=246.957 ms/op, peakRSS=187.4 MiB
youtube/retained/1/moejs repeat=2: CPU=238.809 ms/op, peakRSS=195.7 MiB
youtube/retained/1/goja repeat=2: CPU=264.583 ms/op, peakRSS=203.7 MiB
youtube/retained/1/goja repeat=3: CPU=276.460 ms/op, peakRSS=205.2 MiB
youtube/retained/1/moejs repeat=3: CPU=244.340 ms/op, peakRSS=193.2 MiB
youtube/retained/1/moejs repeat=4: CPU=242.499 ms/op, peakRSS=193.5 MiB
youtube/retained/1/goja repeat=4: CPU=266.238 ms/op, peakRSS=204.8 MiB
youtube/retained/1/goja repeat=5: CPU=271.543 ms/op, peakRSS=204.9 MiB
youtube/retained/1/moejs repeat=5: CPU=246.237 ms/op, peakRSS=189.5 MiB
youtube/retained/8/goja repeat=1: CPU=301.610 ms/op, peakRSS=617.3 MiB
youtube/retained/8/moejs repeat=1: CPU=235.625 ms/op, peakRSS=674.7 MiB
youtube/retained/8/moejs repeat=2: CPU=239.678 ms/op, peakRSS=670.2 MiB
youtube/retained/8/goja repeat=2: CPU=292.897 ms/op, peakRSS=615.8 MiB
youtube/retained/8/goja repeat=3: CPU=294.733 ms/op, peakRSS=674.6 MiB
youtube/retained/8/moejs repeat=3: CPU=233.436 ms/op, peakRSS=618.6 MiB
youtube/retained/8/moejs repeat=4: CPU=236.478 ms/op, peakRSS=679.0 MiB
youtube/retained/8/goja repeat=4: CPU=297.068 ms/op, peakRSS=668.8 MiB
youtube/retained/8/goja repeat=5: CPU=297.040 ms/op, peakRSS=676.7 MiB
youtube/retained/8/moejs repeat=5: CPU=241.736 ms/op, peakRSS=706.0 MiB
twitter/cold/20/goja repeat=1: CPU=15.356 ms/op, peakRSS=79.7 MiB
twitter/cold/20/moejs repeat=1: CPU=10.555 ms/op, peakRSS=97.0 MiB
twitter/cold/20/moejs repeat=2: CPU=10.767 ms/op, peakRSS=82.5 MiB
twitter/cold/20/goja repeat=2: CPU=15.595 ms/op, peakRSS=78.9 MiB
twitter/cold/20/goja repeat=3: CPU=15.555 ms/op, peakRSS=79.9 MiB
twitter/cold/20/moejs repeat=3: CPU=10.720 ms/op, peakRSS=97.0 MiB
twitter/cold/20/moejs repeat=4: CPU=10.594 ms/op, peakRSS=82.3 MiB
twitter/cold/20/goja repeat=4: CPU=15.477 ms/op, peakRSS=80.2 MiB
twitter/cold/20/goja repeat=5: CPU=15.898 ms/op, peakRSS=79.0 MiB
twitter/cold/20/moejs repeat=5: CPU=10.541 ms/op, peakRSS=96.8 MiB
twitter/warm/200/goja repeat=1: CPU=3.278 ms/op, peakRSS=78.7 MiB
twitter/warm/200/moejs repeat=1: CPU=0.455 ms/op, peakRSS=76.4 MiB
twitter/warm/200/moejs repeat=2: CPU=0.452 ms/op, peakRSS=96.2 MiB
twitter/warm/200/goja repeat=2: CPU=3.265 ms/op, peakRSS=78.8 MiB
twitter/warm/200/goja repeat=3: CPU=3.223 ms/op, peakRSS=97.6 MiB
twitter/warm/200/moejs repeat=3: CPU=0.453 ms/op, peakRSS=76.4 MiB
twitter/warm/200/moejs repeat=4: CPU=0.453 ms/op, peakRSS=96.1 MiB
twitter/warm/200/goja repeat=4: CPU=3.276 ms/op, peakRSS=78.8 MiB
twitter/warm/200/goja repeat=5: CPU=3.201 ms/op, peakRSS=78.7 MiB
twitter/warm/200/moejs repeat=5: CPU=0.455 ms/op, peakRSS=76.7 MiB
twitter/retained/1/goja repeat=1: CPU=16.125 ms/op, peakRSS=75.2 MiB
twitter/retained/1/moejs repeat=1: CPU=11.682 ms/op, peakRSS=74.5 MiB
twitter/retained/1/moejs repeat=2: CPU=11.636 ms/op, peakRSS=74.6 MiB
twitter/retained/1/goja repeat=2: CPU=15.417 ms/op, peakRSS=75.1 MiB
twitter/retained/1/goja repeat=3: CPU=14.978 ms/op, peakRSS=74.8 MiB
twitter/retained/1/moejs repeat=3: CPU=11.520 ms/op, peakRSS=94.1 MiB
twitter/retained/1/moejs repeat=4: CPU=11.398 ms/op, peakRSS=74.6 MiB
twitter/retained/1/goja repeat=4: CPU=15.235 ms/op, peakRSS=94.5 MiB
twitter/retained/1/goja repeat=5: CPU=15.355 ms/op, peakRSS=75.3 MiB
twitter/retained/1/moejs repeat=5: CPU=11.538 ms/op, peakRSS=94.1 MiB
twitter/retained/8/goja repeat=1: CPU=19.502 ms/op, peakRSS=106.9 MiB
twitter/retained/8/moejs repeat=1: CPU=12.182 ms/op, peakRSS=93.0 MiB
twitter/retained/8/moejs repeat=2: CPU=12.171 ms/op, peakRSS=92.9 MiB
twitter/retained/8/goja repeat=2: CPU=18.439 ms/op, peakRSS=98.1 MiB
twitter/retained/8/goja repeat=3: CPU=19.251 ms/op, peakRSS=108.3 MiB
twitter/retained/8/moejs repeat=3: CPU=12.528 ms/op, peakRSS=96.6 MiB
twitter/retained/8/moejs repeat=4: CPU=12.132 ms/op, peakRSS=93.3 MiB
twitter/retained/8/goja repeat=4: CPU=19.235 ms/op, peakRSS=108.2 MiB
twitter/retained/8/goja repeat=5: CPU=18.473 ms/op, peakRSS=98.1 MiB
twitter/retained/8/moejs repeat=5: CPU=12.258 ms/op, peakRSS=93.1 MiB
Loaded 50 of 349 files, more files were not shown because too many files have changed in this diff. Show more