diff --git a/src/external-patches/firefox/session_store_use_size_hint/D247215.patch b/src/external-patches/firefox/session_store_use_size_hint/D247215.patch deleted file mode 100644 index 53e619863..000000000 --- a/src/external-patches/firefox/session_store_use_size_hint/D247215.patch +++ /dev/null @@ -1,251 +0,0 @@ -diff --git a/dom/chrome-webidl/IOUtils.webidl b/dom/chrome-webidl/IOUtils.webidl ---- a/dom/chrome-webidl/IOUtils.webidl -+++ b/dom/chrome-webidl/IOUtils.webidl -@@ -94,23 +94,23 @@ - * otherwise rejects with a DOMException. - */ - [NewObject] - Promise writeUTF8(DOMString path, UTF8String string, optional WriteOptions options = {}); - /** -- * Attempts to serialize |value| into a JSON string and encode it as into a -- * UTF-8 string, then safely write the result to a file at |path|. Works -- * exactly like |write|. -+ * Attempts to serialize |value| into a JSON string and encode it as a UTF-8 -+ * string, then safely write the result to a file at |path|. Works exactly -+ * like |write|. - * - * @param path An absolute file path - * @param value The value to be serialized. - * @param options Options for writing the file. The "append" mode is not supported. - * - * @return Resolves with the number of bytes successfully written to the file, - * otherwise rejects with a DOMException. - */ - [NewObject] -- Promise writeJSON(DOMString path, any value, optional WriteOptions options = {}); -+ Promise writeJSON(DOMString path, any value, optional WriteJSONOptions options = {}); - /** - * Moves the file from |sourcePath| to |destPath|, creating necessary parents. - * If |destPath| is a directory, then the source file will be moved into the - * destination directory. - * -@@ -567,10 +567,39 @@ - * If true, compress the data with LZ4-encoding before writing to the file. - */ - boolean compress = false; - }; - -+/** -+ * Options to be passed to the |IOUtils.writeJSON| method. -+ */ -+dictionary WriteJSONOptions: WriteOptions { -+ /** -+ * An optional length hint that will be used to pre-allocate the buffer that -+ * will hold the stringified JSON. -+ * -+ * This is the *length* and not the size (i.e., it is the number of UTF-16 -+ * codepoints and not the number of bytes). -+ */ -+ unsigned long long lengthHint = 0; -+}; -+ -+/** -+ * Information about a WriteJSON operation. -+ */ -+dictionary WriteJSONResult { -+ /** -+ * The number of bytes written. -+ */ -+ required unsigned long long size; -+ -+ /** -+ * The length of the stringified JSON (in UTF-16 codepoints). -+ */ -+ required unsigned long long jsonLength; -+}; -+ - /** - * Options to be passed to the |IOUtils.move| method. - */ - dictionary MoveOptions { - /** -diff --git a/xpcom/ioutils/IOUtils.h b/xpcom/ioutils/IOUtils.h ---- a/xpcom/ioutils/IOUtils.h -+++ b/xpcom/ioutils/IOUtils.h -@@ -94,11 +94,11 @@ - const nsACString& aString, const dom::WriteOptions& aOptions, - ErrorResult& aError); - - static already_AddRefed WriteJSON( - dom::GlobalObject& aGlobal, const nsAString& aPath, -- JS::Handle aValue, const dom::WriteOptions& aOptions, -+ JS::Handle aValue, const dom::WriteJSONOptions& aOptions, - ErrorResult& aError); - - static already_AddRefed Move(dom::GlobalObject& aGlobal, - const nsAString& aSourcePath, - const nsAString& aDestPath, -@@ -736,13 +736,16 @@ - RefPtr mBackupFile; - RefPtr mTmpFile; - dom::WriteMode mMode; - bool mFlush = false; - bool mCompress = false; -+ size_t mLengthHint = 0; - - static Result FromBinding( - const dom::WriteOptions& aOptions); -+ static Result FromBinding( -+ const dom::WriteJSONOptions& aOptions); - }; - - /** - * Re-implements the file compression and decompression utilities found - * in toolkit/components/lz4/lz4.js -diff --git a/xpcom/ioutils/IOUtils.cpp b/xpcom/ioutils/IOUtils.cpp ---- a/xpcom/ioutils/IOUtils.cpp -+++ b/xpcom/ioutils/IOUtils.cpp -@@ -589,15 +589,21 @@ - return WriteSync(file, AsBytes(Span(str)), opts); - }); - }); - } - -+static bool AppendJSON(const char16_t* aBuf, uint32_t aLen, void* aStr) { -+ nsAString* str = static_cast(aStr); -+ -+ return str->Append(aBuf, aLen, fallible); -+} -+ - /* static */ - already_AddRefed IOUtils::WriteJSON(GlobalObject& aGlobal, - const nsAString& aPath, - JS::Handle aValue, -- const WriteOptions& aOptions, -+ const WriteJSONOptions& aOptions, - ErrorResult& aError) { - return WithPromiseAndState( - aGlobal, aError, [&](Promise* promise, auto& state) { - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise, -@@ -623,14 +629,15 @@ - file->HumanReadablePath().get())); - return; - } - - JSContext* cx = aGlobal.Context(); -- JS::Rooted rootedValue(cx, aValue); -+ JS::Rooted value(cx, aValue); - nsString string; -- if (!nsContentUtils::StringifyJSON(cx, aValue, string, -- UndefinedIsNullStringLiteral)) { -+ if (!JS_StringifyWithLengthHint(cx, &value, nullptr, -+ JS::NullHandleValue, AppendJSON, -+ &string, opts.mLengthHint)) { - JS::Rooted exn(cx, JS::UndefinedValue()); - if (JS_GetPendingException(cx, &exn)) { - JS_ClearPendingException(cx); - promise->MaybeReject(exn); - } else { -@@ -639,22 +646,29 @@ - "Could not serialize object to JSON"_ns)); - } - return; - } - -- DispatchAndResolve( -+ DispatchAndResolve( - state->mEventQueue, promise, - [file = std::move(file), string = std::move(string), -- opts = std::move(opts)]() -> Result { -+ opts = std::move(opts)]() -> Result { - nsAutoCString utf8Str; - if (!CopyUTF16toUTF8(string, utf8Str, fallible)) { - return Err(IOError( - NS_ERROR_OUT_OF_MEMORY, - "Failed to write to `%s': could not allocate buffer", - file->HumanReadablePath().get())); - } -- return WriteSync(file, AsBytes(Span(utf8Str)), opts); -+ -+ uint32_t size = -+ MOZ_TRY(WriteSync(file, AsBytes(Span(utf8Str)), opts)); -+ -+ dom::WriteJSONResult result; -+ result.mSize = size; -+ result.mJsonLength = static_cast(string.Length()); -+ return result; - }); - }); - } - - /* static */ -@@ -2840,10 +2854,20 @@ - - opts.mCompress = aOptions.mCompress; - return opts; - } - -+Result -+IOUtils::InternalWriteOpts::FromBinding(const WriteJSONOptions& aOptions) { -+ InternalWriteOpts opts = -+ MOZ_TRY(FromBinding(static_cast(aOptions))); -+ -+ opts.mLengthHint = aOptions.mLengthHint; -+ -+ return opts; -+} -+ - /* static */ - Result IOUtils::JsBuffer::Create( - IOUtils::BufferKind aBufferKind, size_t aCapacity) { - JsBuffer buffer(aBufferKind, aCapacity); - if (aCapacity != 0 && !buffer.mBuffer) { -diff --git a/xpcom/ioutils/tests/test_ioutils_read_write_json.html b/xpcom/ioutils/tests/test_ioutils_read_write_json.html ---- a/xpcom/ioutils/tests/test_ioutils_read_write_json.html -+++ b/xpcom/ioutils/tests/test_ioutils_read_write_json.html -@@ -140,10 +140,43 @@ - ); - - await cleanup(filename); - }); - -+ add_task(async function test_writeJSON_return() { -+ const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_writeJSON_return.tmp"); -+ -+ const obj = { emoji: "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐" }; -+ -+ const expectedJson = JSON.stringify(obj); -+ const size = new TextEncoder().encode(expectedJson).byteLength; -+ -+ { -+ const result = await IOUtils.writeJSON(filename, obj, { lengthHint: 0 }); -+ -+ is(await IOUtils.readUTF8(filename), expectedJson, "should have written expected JSON"); -+ -+ is(typeof result, "object", "writeJSON returns an object"); -+ ok(result !== null, "writeJSON returns non-null"); -+ -+ ok(Object.hasOwn(result, "size"), "result has size property"); -+ ok(Object.hasOwn(result, "jsonLength"), "result has jsonLength property"); -+ -+ is(result.size, size, "Should have written the expected number of bytes"); -+ is(result.jsonLength, expectedJson.length, "Should have written the expected number of UTF-16 codepoints"); -+ } -+ -+ { -+ const result = await IOUtils.writeJSON(filename, obj, { lengthHint: expectedJson.length, compress: true }); -+ -+ isnot(result.size, size, "Should have written a different number of bytes due to compression"); -+ is(result.jsonLength, expectedJson.length, "Should have written the same number of UTF-16 codepoints"); -+ } -+ -+ await cleanup(filename); -+ }); -+ - add_task(async function test_append_json() { - const filename = PathUtils.join(PathUtils.tempDir, "test_ioutils_append_json.tmp"); - - await IOUtils.writeJSON(filename, OBJECT); - - diff --git a/src/external-patches/firefox/session_store_use_size_hint/D247217.patch b/src/external-patches/firefox/session_store_use_size_hint/D247217.patch deleted file mode 100644 index 31ffe06f9..000000000 --- a/src/external-patches/firefox/session_store_use_size_hint/D247217.patch +++ /dev/null @@ -1,279 +0,0 @@ -diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs ---- a/browser/components/sessionstore/SessionFile.sys.mjs -+++ b/browser/components/sessionstore/SessionFile.sys.mjs -@@ -503,10 +503,12 @@ - if (isFinalWrite) { - Services.obs.notifyObservers( - null, - "sessionstore-final-state-write-complete" - ); -+ -+ lazy.SessionWriter.deinit(); - } - }); - }, - - async wipe() { -diff --git a/browser/components/sessionstore/SessionWriter.sys.mjs b/browser/components/sessionstore/SessionWriter.sys.mjs ---- a/browser/components/sessionstore/SessionWriter.sys.mjs -+++ b/browser/components/sessionstore/SessionWriter.sys.mjs -@@ -6,10 +6,12 @@ - - ChromeUtils.defineESModuleGetters(lazy, { - sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs", - }); - -+const BROWSER_PURGE_SESSION_HISTORY = "browser:purge-session-history"; -+ - /** - * We just started (we haven't written anything to disk yet) from - * `Paths.clean`. The backup directory may not exist. - */ - const STATE_CLEAN = "clean"; -@@ -58,10 +60,14 @@ - export const SessionWriter = { - init(origin, useOldExtension, paths, prefs = {}) { - return SessionWriterInternal.init(origin, useOldExtension, paths, prefs); - }, - -+ deinit() { -+ return SessionWriterInternal.deinit(); -+ }, -+ - /** - * Write the contents of the session file. - * - * @param state - May get changed on shutdown. - */ -@@ -80,10 +86,17 @@ - return await SessionWriterInternal.wipe(); - } finally { - unlock(); - } - }, -+ -+ /** -+ * *Test Only* Return the SessionWriter's length hint for writing JSON. -+ */ -+ get _jsonLengthHint() { -+ return SessionWriterInternal.jsonLengthHint; -+ }, - }; - - const SessionWriterInternal = { - // Path to the files used by the SessionWriter - Paths: null, -@@ -104,10 +117,19 @@ - /** - * Number of old upgrade backups that are being kept - */ - maxUpgradeBackups: null, - -+ /** -+ * The size of the last write with IOUtils.writeJSON. -+ * -+ * Because SessionWriter writes such a large object graph we will otherwise -+ * spend a large portion of `write()` doing memory allocations and memcpy -+ * when serializing the session file to disk. -+ */ -+ jsonLengthHint: 0, -+ - /** - * Initialize (or reinitialize) the writer. - * - * @param {string} origin Which of sessionstore.js or its backups - * was used. One of the `STATE_*` constants defined above. -@@ -136,13 +158,20 @@ - this.Paths = paths; - this.maxUpgradeBackups = prefs.maxUpgradeBackups; - this.maxSerializeBack = prefs.maxSerializeBack; - this.maxSerializeForward = prefs.maxSerializeForward; - this.upgradeBackupNeeded = paths.nextUpgradeBackup != paths.upgradeBackup; -+ -+ Services.obs.addObserver(this, BROWSER_PURGE_SESSION_HISTORY); -+ - return { result: true }; - }, - -+ deinit() { -+ Services.obs.removeObserver(this, BROWSER_PURGE_SESSION_HISTORY); -+ }, -+ - /** - * Write the session to disk. - * Write the session to disk, performing any necessary backup - * along the way. - * -@@ -208,36 +237,42 @@ - // We are shutting down. At this stage, we know that - // $Paths.clean is either absent or corrupted. If it was - // originally present and valid, it has been moved to - // $Paths.cleanBackup a long time ago. We can therefore write - // with the guarantees that we erase no important data. -- await IOUtils.writeJSON(this.Paths.clean, state, { -+ const result = await IOUtils.writeJSON(this.Paths.clean, state, { - tmpPath: this.Paths.clean + ".tmp", - compress: true, -+ lengthHint: this.jsonLengthHint, - }); -+ this.jsonLengthHint = result.jsonLength; - fileStat = await IOUtils.stat(this.Paths.clean); - } else if (this.state == STATE_RECOVERY) { - // At this stage, either $Paths.recovery was written >= 15 - // seconds ago during this session or we have just started - // from $Paths.recovery left from the previous session. Either - // way, $Paths.recovery is good. We can move $Path.backup to - // $Path.recoveryBackup without erasing a good file with a bad - // file. -- await IOUtils.writeJSON(this.Paths.recovery, state, { -+ const result = await IOUtils.writeJSON(this.Paths.recovery, state, { - tmpPath: this.Paths.recovery + ".tmp", - backupFile: this.Paths.recoveryBackup, - compress: true, -+ lengthHint: this.jsonLengthHint, - }); -+ this.jsonLengthHint = result.jsonLength; - fileStat = await IOUtils.stat(this.Paths.recovery); - } else { - // In other cases, either $Path.recovery is not necessary, or - // it doesn't exist or it has been corrupted. Regardless, - // don't backup $Path.recovery. -- await IOUtils.writeJSON(this.Paths.recovery, state, { -+ const result = await IOUtils.writeJSON(this.Paths.recovery, state, { - tmpPath: this.Paths.recovery + ".tmp", - compress: true, -+ lengthHint: this.jsonLengthHint, - }); -+ this.jsonLengthHint = result.jsonLength; - fileStat = await IOUtils.stat(this.Paths.recovery); - } - - telemetry.writeFileMs = Date.now() - startWriteMs; - telemetry.fileSizeBytes = fileStat.size; -@@ -420,6 +455,18 @@ - - if (exn) { - throw exn; - } - }, -+ -+ observe(_subject, topic, _data) { -+ switch (topic) { -+ case BROWSER_PURGE_SESSION_HISTORY: -+ this._onPurgeSessionHistory(); -+ break; -+ } -+ }, -+ -+ _onPurgeSessionHistory() { -+ this.jsonLengthHint = 0; -+ }, - }; -diff --git a/browser/components/sessionstore/test/unit/test_write_json_length_hint.js b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js -new file mode 100644 ---- /dev/null -+++ b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js -@@ -0,0 +1,91 @@ -+/* Any copyright is dedicated to the Public Domain. -+ http://creativecommons.org/publicdomain/zero/1.0/ */ -+ -+"use strict"; -+ -+const { updateAppInfo } = ChromeUtils.importESModule( -+ "resource://testing-common/AppInfo.sys.mjs" -+); -+ -+const profile = do_get_profile(); -+ -+updateAppInfo({ -+ name: "SessionRestoreTest", -+ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}", -+ version: "1", -+ platformVersion: "", -+}); -+ -+const { SessionFile } = ChromeUtils.importESModule( -+ "resource:///modules/sessionstore/SessionFile.sys.mjs" -+); -+const { SessionWriter } = ChromeUtils.importESModule( -+ "resource:///modules/sessionstore/SessionWriter.sys.mjs" -+); -+ -+add_setup(async function setup() { -+ const source = do_get_file("data/sessionstore_valid.js"); -+ source.copyTo(profile, "sessionstore.js"); -+ -+ await writeCompressedFile( -+ SessionFile.Paths.clean.replace("jsonlz4", "js"), -+ SessionFile.Paths.clean -+ ); -+ -+ await SessionFile.read(); -+}); -+ -+add_task(async function test_json_length_hint() { -+ await IOUtils.writeJSON(PathUtils.join(PathUtils.profileDir, "dingus"), { -+ gunk: true, -+ }); -+ -+ Assert.equal( -+ SessionWriter._jsonLengthHint, -+ 0, -+ "SessionWriter length hint starts at 0" -+ ); -+ -+ await SessionFile.write({}); -+ -+ const lengthHint = SessionWriter._jsonLengthHint; -+ -+ Assert.equal( -+ SessionWriter._jsonLengthHint, -+ JSON.stringify({}).length, -+ "SessionWriter should cache length hint" -+ ); -+ -+ const contents = await IOUtils.readJSON( -+ PathUtils.join(do_get_cwd().path, "data", "sessionstore_complete.json") -+ ); -+ await SessionFile.write(contents); -+ -+ Assert.notEqual( -+ SessionWriter._jsonLengthHint, -+ lengthHint, -+ "SessionWriter length hint updated" -+ ); -+ -+ Assert.greater( -+ SessionWriter._jsonLengthHint, -+ lengthHint, -+ "SessionWriteLength hint is now larger" -+ ); -+ -+ Services.obs.notifyObservers(null, "browser:purge-session-history"); -+ -+ Assert.equal( -+ SessionWriter._jsonLengthHint, -+ 0, -+ "browser:purge-session-history notification cleans length hint" -+ ); -+ -+ await SessionFile.write(contents); -+ -+ Assert.notEqual( -+ SessionWriter._jsonLengthHint, -+ lengthHint, -+ "SessionWriter length hint updated" -+ ); -+}); -diff --git a/browser/components/sessionstore/test/unit/xpcshell.toml b/browser/components/sessionstore/test/unit/xpcshell.toml ---- a/browser/components/sessionstore/test/unit/xpcshell.toml -+++ b/browser/components/sessionstore/test/unit/xpcshell.toml -@@ -39,5 +39,7 @@ - skip-if = [ - "condprof", # Bug 1769154 - ] - - ["test_startup_session_async.js"] -+ -+["test_write_json_length_hint.js"] - diff --git a/src/external-patches/firefox/session_store_use_size_hint/D298708.patch b/src/external-patches/firefox/session_store_use_size_hint/D298708.patch new file mode 100644 index 000000000..965ab02c5 --- /dev/null +++ b/src/external-patches/firefox/session_store_use_size_hint/D298708.patch @@ -0,0 +1,280 @@ +diff --git a/browser/components/sessionstore/SessionWriter.sys.mjs b/browser/components/sessionstore/SessionWriter.sys.mjs +--- a/browser/components/sessionstore/SessionWriter.sys.mjs ++++ b/browser/components/sessionstore/SessionWriter.sys.mjs +@@ -80,10 +80,14 @@ + return await SessionWriterInternal.wipe(); + } finally { + unlock(); + } + }, ++ ++ get _jsonLengthHint() { ++ return SessionWriterInternal._lastJsonLength; ++ }, + }; + + const SessionWriterInternal = { + // Path to the files used by the SessionWriter + Paths: null, +@@ -104,10 +108,14 @@ + /** + * Number of old upgrade backups that are being kept + */ + maxUpgradeBackups: null, + ++ // Estimated JSON string length from the previous write, used to pre-size ++ // the serialization buffer and avoid incremental reallocations. ++ _lastJsonLength: 0, ++ + /** + * Initialize (or reinitialize) the writer. + * + * @param {string} origin Which of sessionstore.js or its backups + * was used. One of the `STATE_*` constants defined above. +@@ -201,48 +209,60 @@ + } + } + + let startWriteMs = Date.now(); + let fileStat; ++ // Add 5% headroom to the hint so small growth between saves doesn't ++ // cause reallocs. The compressed-size-based estimate already has ++ // sufficient margin from the 4x multiplier. ++ let jsonLengthHint = Math.ceil(this._lastJsonLength * 1.05); ++ ++ let uncompressedBytes; + + if (options.isFinalWrite) { + // We are shutting down. At this stage, we know that + // $Paths.clean is either absent or corrupted. If it was + // originally present and valid, it has been moved to + // $Paths.cleanBackup a long time ago. We can therefore write + // with the guarantees that we erase no important data. +- await IOUtils.writeJSON(this.Paths.clean, state, { ++ uncompressedBytes = await IOUtils.writeJSON(this.Paths.clean, state, { + tmpPath: this.Paths.clean + ".tmp", + compress: true, ++ jsonLengthHint, + }); + fileStat = await IOUtils.stat(this.Paths.clean); + } else if (this.state == STATE_RECOVERY) { + // At this stage, either $Paths.recovery was written >= 15 + // seconds ago during this session or we have just started + // from $Paths.recovery left from the previous session. Either + // way, $Paths.recovery is good. We can move $Path.backup to + // $Path.recoveryBackup without erasing a good file with a bad + // file. +- await IOUtils.writeJSON(this.Paths.recovery, state, { ++ uncompressedBytes = await IOUtils.writeJSON(this.Paths.recovery, state, { + tmpPath: this.Paths.recovery + ".tmp", + backupFile: this.Paths.recoveryBackup, + compress: true, ++ jsonLengthHint, + }); + fileStat = await IOUtils.stat(this.Paths.recovery); + } else { + // In other cases, either $Path.recovery is not necessary, or + // it doesn't exist or it has been corrupted. Regardless, + // don't backup $Path.recovery. +- await IOUtils.writeJSON(this.Paths.recovery, state, { ++ uncompressedBytes = await IOUtils.writeJSON(this.Paths.recovery, state, { + tmpPath: this.Paths.recovery + ".tmp", + compress: true, ++ jsonLengthHint, + }); + fileStat = await IOUtils.stat(this.Paths.recovery); + } + + telemetry.writeFileMs = Date.now() - startWriteMs; + telemetry.fileSizeBytes = fileStat.size; ++ // Use the actual pre-compression size from this write as the hint ++ // for the next write's buffer allocation. ++ this._lastJsonLength = uncompressedBytes; + lazy.sessionStoreLogger.debug( + `SessionWriter.write wrote ${telemetry.fileSizeBytes} bytes in ${telemetry.writeFileMs}ms` + ); + } catch (ex) { + // Don't throw immediately +@@ -375,10 +395,11 @@ + } catch (ex) { + exn = exn || ex; + } + + this.state = STATE_EMPTY; ++ this._lastJsonLength = 0; + if (exn) { + throw exn; + } + + return { result: true }; +diff --git a/browser/components/sessionstore/test/unit/test_write_json_length_hint.js b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js +new file mode 100644 +--- /dev/null ++++ b/browser/components/sessionstore/test/unit/test_write_json_length_hint.js +@@ -0,0 +1,73 @@ ++/* Any copyright is dedicated to the Public Domain. ++ http://creativecommons.org/publicdomain/zero/1.0/ */ ++ ++"use strict"; ++ ++const { SessionWriter } = ChromeUtils.importESModule( ++ "resource:///modules/sessionstore/SessionWriter.sys.mjs" ++); ++ ++const profd = do_get_profile(); ++const { SessionFile } = ChromeUtils.importESModule( ++ "resource:///modules/sessionstore/SessionFile.sys.mjs" ++); ++ ++const { updateAppInfo } = ChromeUtils.importESModule( ++ "resource://testing-common/AppInfo.sys.mjs" ++); ++updateAppInfo({ ++ name: "SessionRestoreTest", ++ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}", ++ version: "1", ++ platformVersion: "", ++}); ++ ++add_setup(async function () { ++ let source = do_get_file("data/sessionstore_valid.js"); ++ source.copyTo(profd, "sessionstore.js"); ++ await writeCompressedFile( ++ SessionFile.Paths.clean.replace("jsonlz4", "js"), ++ SessionFile.Paths.clean ++ ); ++ await SessionFile.read(); ++}); ++ ++add_task(async function test_length_hint_updates_after_write() { ++ Assert.equal( ++ SessionWriter._jsonLengthHint, ++ 0, ++ "Length hint starts at 0" ++ ); ++ ++ await SessionFile.write({}); ++ ++ let hintAfterSmall = SessionWriter._jsonLengthHint; ++ Assert.equal( ++ hintAfterSmall, ++ JSON.stringify({}).length, ++ "Hint matches the uncompressed JSON byte length" ++ ); ++ ++ let largerState = await IOUtils.readJSON( ++ PathUtils.join(do_get_cwd().path, "data", "sessionstore_complete.json") ++ ); ++ await SessionFile.write(largerState); ++ ++ Assert.greater( ++ SessionWriter._jsonLengthHint, ++ hintAfterSmall, ++ "Hint grows after writing a larger state" ++ ); ++}); ++ ++add_task(async function test_length_hint_resets_on_wipe() { ++ await SessionFile.write({ windows: [{ tabs: [{ entries: [] }] }] }); ++ Assert.greater(SessionWriter._jsonLengthHint, 0, "Hint is nonzero"); ++ ++ await SessionFile.wipe(); ++ Assert.equal( ++ SessionWriter._jsonLengthHint, ++ 0, ++ "Hint resets to 0 after wipe" ++ ); ++}); +diff --git a/browser/components/sessionstore/test/unit/xpcshell.toml b/browser/components/sessionstore/test/unit/xpcshell.toml +--- a/browser/components/sessionstore/test/unit/xpcshell.toml ++++ b/browser/components/sessionstore/test/unit/xpcshell.toml +@@ -39,5 +39,10 @@ + skip-if = [ + "condprof", # Bug 1769154 + ] + + ["test_startup_session_async.js"] ++ ++["test_write_json_length_hint.js"] ++support-files = [ ++ "data/sessionstore_complete.json", ++] +diff --git a/dom/chrome-webidl/IOUtils.webidl b/dom/chrome-webidl/IOUtils.webidl +--- a/dom/chrome-webidl/IOUtils.webidl ++++ b/dom/chrome-webidl/IOUtils.webidl +@@ -101,12 +101,12 @@ + * + * @param path An absolute file path + * @param value The value to be serialized. + * @param options Options for writing the file. The "append" mode is not supported. + * +- * @return Resolves with the number of bytes successfully written to the file, +- * otherwise rejects with a DOMException. ++ * @return Resolves with the pre-compression size of the serialized JSON in ++ * bytes (UTF-8), otherwise rejects with a DOMException. + */ + [NewObject] + Promise writeJSON(DOMString path, any value, optional WriteOptions options = {}); + /** + * Moves the file from |sourcePath| to |destPath|, creating necessary parents. +@@ -564,10 +564,16 @@ + boolean flush = false; + /** + * If true, compress the data with LZ4-encoding before writing to the file. + */ + boolean compress = false; ++ /** ++ * For |writeJSON|, a hint for the expected JSON string length in UTF-16 code ++ * units. When provided, the JSON serializer pre-allocates a buffer of this ++ * size to avoid incremental reallocations. ++ */ ++ unsigned long long jsonLengthHint = 0; + }; + + /** + * Options to be passed to the |IOUtils.move| method. + */ +diff --git a/xpcom/ioutils/IOUtils.cpp b/xpcom/ioutils/IOUtils.cpp +--- a/xpcom/ioutils/IOUtils.cpp ++++ b/xpcom/ioutils/IOUtils.cpp +@@ -622,13 +622,22 @@ + return; + } + + JSContext* cx = aGlobal.Context(); + JS::Rooted rootedValue(cx, aValue); ++ size_t lengthHint = aOptions.mJsonLengthHint; + nsString string; +- if (!nsContentUtils::StringifyJSON(cx, aValue, string, +- UndefinedIsNullStringLiteral)) { ++ if (lengthHint) { ++ string.SetCapacity(lengthHint); ++ } ++ if (!JS_StringifyWithLengthHint( ++ cx, &rootedValue, nullptr, JS::NullHandleValue, ++ [](const char16_t* aBuf, uint32_t aLen, void* aData) -> bool { ++ return static_cast(aData)->Append(aBuf, aLen, ++ fallible); ++ }, ++ &string, lengthHint)) { + JS::Rooted exn(cx, JS::UndefinedValue()); + if (JS_GetPendingException(cx, &exn)) { + JS_ClearPendingException(cx); + promise->MaybeReject(exn); + } else { +@@ -648,11 +657,13 @@ + return Err(IOError( + NS_ERROR_OUT_OF_MEMORY, + "Failed to write to `%s': could not allocate buffer", + file->HumanReadablePath().get())); + } +- return WriteSync(file, AsBytes(Span(utf8Str)), opts); ++ uint32_t uncompressedSize = utf8Str.Length(); ++ MOZ_TRY(WriteSync(file, AsBytes(Span(utf8Str)), opts)); ++ return uncompressedSize; + }); + }); + } + + /* static */ + diff --git a/src/external-patches/manifest.json b/src/external-patches/manifest.json index 8c67f7d7b..37edc43f6 100644 --- a/src/external-patches/manifest.json +++ b/src/external-patches/manifest.json @@ -34,7 +34,7 @@ } }, { - "type": "file", + "type": "local", // TODO: Convert into https://phabricator.services.mozilla.com/D298079 // once it gets accepted. "path": "firefox/allow_backdrop_to_work_on_transparency.patch" @@ -62,8 +62,7 @@ "type": "phabricator", "ids": [ "D247145", - "D247215", - "D247217" + "D298708" ], "name": "Session store use size hint" }, diff --git a/src/toolkit/modules/JSONFile-sys-mjs.patch b/src/toolkit/modules/JSONFile-sys-mjs.patch index ca854e039..f289c4b40 100644 --- a/src/toolkit/modules/JSONFile-sys-mjs.patch +++ b/src/toolkit/modules/JSONFile-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/toolkit/modules/JSONFile.sys.mjs b/toolkit/modules/JSONFile.sys.mjs -index 397991e4af8f49b6365d729fc11267b5c1113400..1955b7ff1d428e891f5ef066e7a4ac25aa5ec9b4 100644 +index 397991e4af8f49b6365d729fc11267b5c1113400..9b1d6fd3850b239000a3c4d2a2d5799a0989f4e3 100644 --- a/toolkit/modules/JSONFile.sys.mjs +++ b/toolkit/modules/JSONFile.sys.mjs @@ -132,6 +132,7 @@ export function JSONFile(config) { @@ -16,14 +16,14 @@ index 397991e4af8f49b6365d729fc11267b5c1113400..1955b7ff1d428e891f5ef066e7a4ac25 try { - await IOUtils.writeJSON( + if (this._useSizeHints && this._lastSavedSize) { -+ this._options.lengthHint = this._lastSavedSize; ++ this._options.jsonLengthHint = Math.ceil(this._lastSavedSize * 1.05); + } + const result = await IOUtils.writeJSON( this.path, this._data, Object.assign({ tmpPath: this.path + ".tmp" }, this._options) ); -+ this._lastSavedSize = this._useSizeHints ? result.jsonLength : null; ++ this._lastSavedSize = this._useSizeHints ? result : null; } catch (ex) { if (typeof this._data.toJSONSafe == "function") { // If serialization fails, try fallback safe JSON converter.