v3.3.97 (UV_THREADPOOL_SIZE 64→8) was a decisive win — mean event-loop-delay at
70 active uploads dropped 200ms→~11ms (18×), rss 577→287MB, renderer healthy in
14/15 windows. But the user reports it is still not perfectly smooth. A focused
multi-agent investigation plus an adversarial review localized the residual to
TWO distinct, separately-measured spike sources:
1. Read-bursts. In the tail windows the file-read histogram inverts: FSReqCallback
climbs to 66-70 against threadpool=8 (~8.75× queue depth) while SimpleWriteWrap
(socket writes) collapses to 4-24 and mean delay rises to 30-42ms. GC is ruled
out (gcMax ≤27ms in every window). The clean inversion at a stable active=70 /
pending=1287 shows the reads are causal, not a symptom of a block elsewhere.
2. A suspected synchronous config-persist stall. save() → load() reparses the whole
electron-config.json — which now carries the 1287-job pending queue nested in
globalSettings plus full history — on every persist (because _atomicWrite nulls
the read cache), then _serializeForDisk JSON.stringify(…, null, 2) of all of it.
One tail sample (max 1021ms, heap spiking to 142MB) fits a large synchronous
structuredClone+stringify, but it is a single confounded point, so this build
only INSTRUMENTS the path rather than asserting the cause.
This release ships one behavioral change (kept to a single variable so the next
log attributes cleanly) plus measurement:
- highWaterMark 256KB→1MB in all five streaming read loops (lib/hosters.js,
doodstream/voe/vidmoly CHUNK_SIZE consts, and the inline value in
clouddrop-upload.js:108 — NOT the 16MB server chunk at clouddrop-upload.js:12).
UV_THREADPOOL_SIZE stays 8. This deepens each stream's read-ahead cushion from
~0.43s to ~1.7s at the per-stream rate, so a stream tolerates the threadpool
queue without starving its socket write, and cuts read-completion callbacks and
per-chunk Buffer allocations ~4×. Byte-correctness is unaffected: Content-Length
is preamble+fileSize+epilogue, independent of chunk size, and the chunk size
never touches the multipart boundaries. Fully reversible; a dedicated read-
concurrency semaphore is held in reserve if 1MB does not clear the bursts.
- config-store.js now times load() (the full reparse, which the account-failed
handler also hits per failure) and the _commit serialize, logging
`config-load …` / `config-serialize wall=…ms bytes=… hist=… queue=…` when the
synchronous work exceeds 20ms. load() is split into a timing wrapper + _loadImpl;
the timer is a no-op until main.js wires configStore.setPerfLog → logInfo.
The renderer batch-drain fix for the one observed 243ms longtask is intentionally
deferred: that jank is downstream of the main-thread read-burst flooding IPC, so
fix#1 should make it self-heal; bundling it would confound the measurement and
touch the progress hot path. All 397 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captured the real browser upload POST and compared to our request.
Two corrections:
- The file field is named 'file', not 'file_0'. The XFS-indexed
naming was a bad guess — the current transit accepts only 'file'.
- The form also needs 'to_json=1' (forces JSON response instead of
an HTML redirect page, matching what the browser submits) and
'fld_id=0' (destination folder, 0 = root). Dropped upload_type,
srv_tmp_url, utype — those were XFS remnants and aren't part of
the current server's contract.
- Response shape is now { status: 'OK', file_code, msg } instead of
the older { files: [...] } / { result: ... } XFS variants; the
parser handles all three plus carries the server's msg forward
on explicit rejections.
The transit server runs nginx-upload-progress and requires an
X-Progress-ID query parameter on the POST URL to finalize the
upload session. Without it the server accepts all bytes but never
sends the response — matches the reported 99%-stuck behavior. The
browser appends it automatically before submit; we now do the same.
Upload stalled at 99% because we were sending vidmoly.me cookies to
*.vmwesa.online (transit server rejects them silently). Browsers never
send those cross-origin. Now we omit the Cookie header and match the
Origin/Referer the browser uses. Also added the full classic XFS field
set (upload_type, sess_id, srv_tmp_url, utype) in the order the
server's handler expects.
The SPA redesign killed the old XFS form POST at / with op=login.
The new flow is a JSON POST to /api/auth/login that returns a
vidmoly_session HttpOnly cookie, which is what /api/upload/config
actually authenticates against.
After login we also probe /api/upload/config once to fail fast if
the session was issued but not actually valid for uploads.
The old /my HTML check failed because it couldn't distinguish an XFS
session from a full SPA session. Since /api/upload/config is what the
upload actually needs, probe it directly after login — 200 JSON with
sess_id/upload_url means we're good, anything else means we're out.
The Vidmoly SPA redesign removed the /?op=upload HTML form — the old
regex-scrape of hidden inputs no longer works. The site now exposes
GET /api/upload/config which returns { sess_id, upload_url } plus the
allowed extensions. Rewrote getUploadParams() to use that endpoint;
the rest of the multipart upload flow (sess_id + utype + file_0) is
the same classic XFS shape.
Three fixes bundled:
- Vidmoly redesign broke login: the old check required either the
'login' or 'xfsts' cookie, but the new site sets different cookie
names. Now we verify by fetching /?op=my_account and looking for
logged-in markers (Logout / My Account / My Files) in the body
instead of relying on specific cookie names.
- retrySelectedJobs left the stale uploadId in _jobIndexByUploadId
when resetting a job. A late 'aborted'/'error' event from the
original (cancelled) upload could route back to the reset job
and overwrite its 'preview' state. Now the old uploadId is
removed from the index and marked in _deletedJobIds so those
stragglers get dropped.
- toggleAccount did two IPC round-trips (saveConfig + getConfig) on
every enable/disable click, plus four re-renders (Accounts,
HosterSummary, HosterModal, Settings). Rapid clicks felt laggy.
The getConfig refetch is redundant since we mutated the flag in
place, and HosterModal/Settings don't depend on account enabled
state. Click now renders immediately and the save runs async.
- Upload button no longer gets permanently stuck if startUpload()
throws after health check (try-catch with uploading=false reset)
- Wait for running health check instead of silently blocking upload
- Add abort signal check in VOE/Vidmoly upload generators
- Escape filenames with quotes/backslashes in multipart form headers
(all 4 uploaders: doodstream, voe, vidmoly, byse)
- Validate backup import structure before overwriting config
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add max redirect depth (10) to Vidmoly _fetch to prevent stack overflow
- Drain undici response body on redirect to prevent connection leaks
- Fix installUpdate unhandled promise rejection in main.js
- Fix context menu submenu viewport overflow with flip-left CSS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add FIFO semaphore for per-hoster concurrency control
- Add token-bucket speed limiter with abort signal support
- Rewrite upload-manager with retry loop, speed monitoring, and rich progress events
- Add per-hoster settings: retries, max speed, parallel count, restart below speed, time interval, max size
- Add context menu with shutdown-after-finish (sleep/shutdown/restart), always-on-top
- Add z-o-o-m-style queue table with 8 columns, status-colored rows, progress bars
- Add debounced queue rendering with scroll position preservation
- Add statusbar with global speed, total bytes, elapsed time
- Fix speedMonitor interval leak on error and scoping bug
- Fix throttle not respecting abort signal during cancellation
- Fix combined signal listener cleanup
- Bump version to 1.1.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>