aboutsummaryrefslogtreecommitdiff
path: root/source/import/Importer.mc
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-21 03:55:01 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-21 03:55:01 +0200
commita69e23fb5266dd942d125239e7dcbe8a7c8ab956 (patch)
tree164c7ed21b550ac3d5fef567060ffad4e40e4583 /source/import/Importer.mc
parent6039571d70b1f6de1ae384e58e11879869be356c (diff)
perf(import): keep parsed items packed and the pasted text once
A parsed import item with its Entry took about 760 bytes as objects on the Forerunner 255S; packed into one Codec ByteArray it takes about 110, and the importer unpacks one item per store step (wiping the packed secret). Aegis group names are resolved when unpacking, so no step repacks all items. The JSON is parsed from the untrimmed text's bytes and the string is dropped; an incomplete part is saved from those bytes. The form hash is computed before the importer copies the text. The peak of a 100-entry import on the 255S falls from 56 % to 45 %. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/import/Importer.mc')
-rw-r--r--source/import/Importer.mc40
1 files changed, 25 insertions, 15 deletions
diff --git a/source/import/Importer.mc b/source/import/Importer.mc
index 9513358..825221b 100644
--- a/source/import/Importer.mc
+++ b/source/import/Importer.mc
@@ -12,6 +12,10 @@
// STORE one item per step: skip, report, or add to the vault. A crash
// leaves only whole entries.
// The report is complete when step() returns true.
+//
+// Memory: while JSON is parsed the pasted text is held once, as the
+// tokenizer's bytes, and parsed items are kept packed (ImportItem.pack);
+// the M8 numbers are in docs/PERFORMANCE.md.
import Toybox.Lang;
@@ -40,7 +44,8 @@ class Importer extends Jobs.Job {
private var _text as String = "";
private var _uris as Array<String>? = null;
private var _aegis as AegisReader? = null;
- private var _items as Array<ImportItem> = [] as Array<ImportItem>;
+ private var _items as Array<ByteArray> = [] as Array<ByteArray>;
+ private var _groups as Dictionary<String, String> = {} as Dictionary<String, String>;
private var _cursor as Number = 0;
private var _ids as Array<String> = [] as Array<String>;
private var _fingerprints as Dictionary<String, String> = {} as Dictionary<String, String>;
@@ -57,7 +62,7 @@ class Importer extends Jobs.Job {
_now = now;
var keys = vault.keyManager.deviceKeys();
var earlier = ImportBuffer.load(vault.storage(), keys, now);
- _text = earlier + text;
+ _text = earlier.length() > 0 ? earlier + text : text;
detect();
}
@@ -65,9 +70,8 @@ class Importer extends Jobs.Job {
function percent() as Number {
if (phase == P_PARSE) {
var a = _aegis;
- var size = _text.length() > 0 ? _text.length() : 1;
if (a != null) {
- return 40 * a.position() / (size > 0 ? size : 1);
+ return 40 * a.position() / (a.size() > 0 ? a.size() : 1);
}
var u = _uris;
return u != null && u.size() > 0 ? 40 * _cursor / u.size() : 0;
@@ -85,7 +89,10 @@ class Importer extends Jobs.Job {
return;
}
if (trimmed.find("{") == 0) {
- _aegis = new AegisReader(Bytes.fromUtf8(trimmed));
+ // The untrimmed text: an incomplete part is saved exactly as
+ // received (a cut can fall on a space inside a string).
+ _aegis = new AegisReader(Bytes.fromUtf8(_text));
+ _text = "";
return;
}
if (Text.findIgnoreCase(trimmed, OtpAuth.MIGRATION_PREFIX, 0) >= 0) {
@@ -133,31 +140,32 @@ class Importer extends Jobs.Job {
return;
}
if (status == AegisReader.INCOMPLETE) {
- var size = Bytes.fromUtf8(_text).size();
- if (ImportBuffer.save(_vault.storage(), _vault.keyManager.deviceKeys(), _text, _now)) {
- report.waitingChars = _text.length();
+ var data = aegis.data();
+ _aegis = null;
+ if (ImportBuffer.save(_vault.storage(), _vault.keyManager.deviceKeys(), data, _now)) {
+ var text = Bytes.toUtf8(data);
+ report.waitingChars = text != null ? text.length() : data.size();
phase = P_DONE;
} else {
- finishWith("the JSON is incomplete and too long to wait for more (" + size + " bytes, at most "
+ finishWith("the JSON is incomplete and too long to wait for more (" + data.size() + " bytes, at most "
+ ImportBuffer.MAX_TEXT_BYTES + ")");
}
- _text = "";
return;
}
if (status == AegisReader.FAILED) {
+ _aegis = null;
finishWith(aegis.error as String);
- _text = "";
return;
}
_items = aegis.items;
+ _groups = aegis.groups;
_aegis = null;
- _text = "";
startReplace();
return;
}
var uris = _uris as Array<String>;
for (var n = 0; n < URIS_PER_STEP && _cursor < uris.size(); n++) {
- _items.add(OtpAuth.parse(uris[_cursor], _cursor + 1));
+ _items.add(OtpAuth.parse(uris[_cursor], _cursor + 1).pack());
_cursor++;
}
if (_cursor >= uris.size()) {
@@ -239,8 +247,10 @@ class Importer extends Jobs.Job {
phase = P_DONE;
return;
}
- var item = _items[_cursor];
- _items[_cursor] = new ImportItem(item.index); // drop the parsed secret
+ var packed = _items[_cursor];
+ _items[_cursor] = []b;
+ var item = ImportItem.unpack(packed, _groups);
+ Bytes.wipe(packed); // the parsed secret; about 110 bytes
_cursor++;
var kind = _kind;
var note = item.note;