Worker WIP

This commit is contained in:
Bruno Windels 2020-09-10 15:40:30 +01:00
parent fdbc5f3c1d
commit 0bf1723d99
12 changed files with 339 additions and 92 deletions

View file

@ -36,8 +36,7 @@ export class ViewModel extends EventEmitter {
if (!this.disposables) { if (!this.disposables) {
this.disposables = new Disposables(); this.disposables = new Disposables();
} }
this.disposables.track(disposable); return this.disposables.track(disposable);
return disposable;
} }
dispose() { dispose() {

View file

@ -38,7 +38,8 @@ export class RoomViewModel extends ViewModel {
async load() { async load() {
this._room.on("change", this._onRoomChange); this._room.on("change", this._onRoomChange);
try { try {
this._timeline = await this._room.openTimeline(); this._timeline = this._room.openTimeline();
await this._timeline.load();
this._timelineVM = new TimelineViewModel(this.childOptions({ this._timelineVM = new TimelineViewModel(this.childOptions({
room: this._room, room: this._room,
timeline: this._timeline, timeline: this._timeline,

View file

@ -268,7 +268,7 @@ class DecryptionPreparation {
} }
dispose() { dispose() {
this._megolmDecryptionChanges.dispose(); this._megolmDecryptionPreparation.dispose();
} }
} }

View file

@ -21,7 +21,7 @@ import {SessionInfo} from "./decryption/SessionInfo.js";
import {DecryptionPreparation} from "./decryption/DecryptionPreparation.js"; import {DecryptionPreparation} from "./decryption/DecryptionPreparation.js";
import {SessionDecryption} from "./decryption/SessionDecryption.js"; import {SessionDecryption} from "./decryption/SessionDecryption.js";
import {SessionCache} from "./decryption/SessionCache.js"; import {SessionCache} from "./decryption/SessionCache.js";
import {DecryptionWorker} from "./decryption/DecryptionWorker.js"; import {DecryptionWorker, WorkerPool} from "./decryption/DecryptionWorker.js";
function getSenderKey(event) { function getSenderKey(event) {
return event.content?.["sender_key"]; return event.content?.["sender_key"];
@ -40,7 +40,7 @@ export class Decryption {
this._pickleKey = pickleKey; this._pickleKey = pickleKey;
this._olm = olm; this._olm = olm;
// this._decryptor = new DecryptionWorker(new Worker("./src/worker.js")); // this._decryptor = new DecryptionWorker(new Worker("./src/worker.js"));
this._decryptor = new DecryptionWorker(new Worker("worker-3074010154.js")); this._decryptor = new DecryptionWorker(new WorkerPool("worker-1039452087.js", 4));
this._initPromise = this._decryptor.init(); this._initPromise = this._decryptor.init();
} }

View file

@ -14,51 +14,200 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export class DecryptionWorker { import {AbortError} from "../../../../utils/error.js";
class WorkerState {
constructor(worker) { constructor(worker) {
this._worker = worker; this.worker = worker;
this.busy = false;
}
attach(pool) {
this.worker.addEventListener("message", pool);
this.worker.addEventListener("error", pool);
}
detach(pool) {
this.worker.removeEventListener("message", pool);
this.worker.removeEventListener("error", pool);
}
}
class Request {
constructor(message, pool) {
this._promise = new Promise((_resolve, _reject) => {
this._resolve = _resolve;
this._reject = _reject;
});
this._message = message;
this._pool = pool;
this._worker = null;
}
abort() {
if (this._isNotDisposed) {
this._pool._abortRequest(this);
this._dispose();
}
}
response() {
return this._promise;
}
_dispose() {
this._reject = null;
this._resolve = null;
}
get _isNotDisposed() {
return this._resolve && this._reject;
}
}
export class WorkerPool {
constructor(path, amount) {
this._workers = [];
for (let i = 0; i < amount ; ++i) {
const worker = new WorkerState(new Worker(path));
worker.attach(this);
this._workers[i] = worker;
}
this._requests = new Map(); this._requests = new Map();
this._counter = 0; this._counter = 0;
this._worker.addEventListener("message", this); this._pendingFlag = false;
} }
handleEvent(e) { handleEvent(e) {
if (e.type === "message") { if (e.type === "message") {
const message = e.data; const message = e.data;
console.log("worker reply", message);
const request = this._requests.get(message.replyToId); const request = this._requests.get(message.replyToId);
if (request) { if (request) {
if (message.type === "success") { request._worker.busy = false;
request.resolve(message.payload); if (request._isNotDisposed) {
} else if (message.type === "error") { if (message.type === "success") {
request.reject(new Error(message.stack)); request._resolve(message.payload);
} else if (message.type === "error") {
request._reject(new Error(message.stack));
}
request._dispose();
} }
this._requests.delete(message.ref_id); this._requests.delete(message.replyToId);
}
console.log("got worker reply", message, this._requests.size);
this._sendPending();
} else if (e.type === "error") {
console.error("worker error", e);
}
}
_getPendingRequest() {
for (const r of this._requests.values()) {
if (!r._worker) {
return r;
} }
} }
} }
_send(message) { _getFreeWorker() {
for (const w of this._workers) {
if (!w.busy) {
return w;
}
}
}
_sendPending() {
this._pendingFlag = false;
console.log("seeing if there is anything to send", this._requests.size);
let success;
do {
success = false;
const request = this._getPendingRequest();
if (request) {
console.log("sending pending request", request);
const worker = this._getFreeWorker();
if (worker) {
this._sendWith(request, worker);
success = true;
}
}
} while (success);
}
_sendWith(request, worker) {
request._worker = worker;
worker.busy = true;
console.log("sending message to worker", request._message);
worker.worker.postMessage(request._message);
}
_enqueueRequest(message) {
this._counter += 1; this._counter += 1;
message.id = this._counter; message.id = this._counter;
let resolve; const request = new Request(message, this);
let reject; this._requests.set(message.id, request);
const promise = new Promise((_resolve, _reject) => { return request;
resolve = _resolve; }
reject = _reject;
send(message) {
const request = this._enqueueRequest(message);
const worker = this._getFreeWorker();
if (worker) {
this._sendWith(request, worker);
}
return request;
}
// assumes all workers are free atm
sendAll(message) {
const promises = this._workers.map(worker => {
const request = this._enqueueRequest(message);
this._sendWith(request, worker);
return request.response();
}); });
this._requests.set(message.id, {reject, resolve}); return Promise.all(promises);
this._worker.postMessage(message); }
return promise;
dispose() {
for (const w of this._workers) {
w.worker.terminate();
w.detach(this);
}
}
_trySendPendingInNextTick() {
if (!this._pendingFlag) {
this._pendingFlag = true;
Promise.resolve().then(() => {
this._sendPending();
});
}
}
_abortRequest(request) {
request._reject(new AbortError());
if (request._worker) {
request._worker.busy = false;
}
this._requests.delete(request._message.id);
// allow more requests to be aborted before trying to send other pending
this._trySendPendingInNextTick();
}
}
export class DecryptionWorker {
constructor(workerPool) {
this._workerPool = workerPool;
} }
decrypt(session, ciphertext) { decrypt(session, ciphertext) {
const sessionKey = session.export_session(session.first_known_index()); const sessionKey = session.export_session(session.first_known_index());
return this._send({type: "megolm_decrypt", ciphertext, sessionKey}); return this._workerPool.send({type: "megolm_decrypt", ciphertext, sessionKey});
} }
init() { async init() {
return this._send({type: "load_olm", path: "olm_legacy-3232457086.js"}); await this._workerPool.sendAll({type: "load_olm", path: "olm_legacy-3232457086.js"});
// return this._send({type: "load_olm", path: "../lib/olm/olm_legacy.js"}); // return this._send({type: "load_olm", path: "../lib/olm/olm_legacy.js"});
} }
} }

View file

@ -27,6 +27,7 @@ export class SessionDecryption {
this._sessionInfo = sessionInfo; this._sessionInfo = sessionInfo;
this._events = events; this._events = events;
this._decryptor = decryptor; this._decryptor = decryptor;
this._decryptionRequests = decryptor ? [] : null;
} }
async decryptAll() { async decryptAll() {
@ -39,7 +40,16 @@ export class SessionDecryption {
try { try {
const {session} = this._sessionInfo; const {session} = this._sessionInfo;
const ciphertext = event.content.ciphertext; const ciphertext = event.content.ciphertext;
const {plaintext, message_index: messageIndex} = await this._decryptor.decrypt(session, ciphertext); let decryptionResult;
if (this._decryptor) {
const request = this._decryptor.decrypt(session, ciphertext);
this._decryptionRequests.push(request);
decryptionResult = await request.response();
} else {
decryptionResult = session.decrypt(ciphertext);
}
const plaintext = decryptionResult.plaintext;
const messageIndex = decryptionResult.message_index;
let payload; let payload;
try { try {
payload = JSON.parse(plaintext); payload = JSON.parse(plaintext);
@ -54,6 +64,10 @@ export class SessionDecryption {
const result = new DecryptionResult(payload, this._sessionInfo.senderKey, this._sessionInfo.claimedKeys); const result = new DecryptionResult(payload, this._sessionInfo.senderKey, this._sessionInfo.claimedKeys);
results.set(event.event_id, result); results.set(event.event_id, result);
} catch (err) { } catch (err) {
// ignore AbortError from cancelling decryption requests in dispose method
if (err.name === "AbortError") {
return;
}
if (!errors) { if (!errors) {
errors = new Map(); errors = new Map();
} }
@ -65,6 +79,12 @@ export class SessionDecryption {
} }
dispose() { dispose() {
if (this._decryptionRequests) {
for (const r of this._decryptionRequests) {
r.abort();
}
}
// TODO: cancel decryptions here
this._sessionInfo.release(); this._sessionInfo.release();
} }
} }

View file

@ -65,7 +65,8 @@ export class Room extends EventEmitter {
retryEntries.push(new EventEntry(storageEntry, this._fragmentIdComparer)); retryEntries.push(new EventEntry(storageEntry, this._fragmentIdComparer));
} }
} }
await this._decryptEntries(DecryptionSource.Retry, retryEntries, txn); const decryptRequest = this._decryptEntries(DecryptionSource.Retry, retryEntries, txn);
await decryptRequest.complete();
if (this._timeline) { if (this._timeline) {
// only adds if already present // only adds if already present
this._timeline.replaceEntries(retryEntries); this._timeline.replaceEntries(retryEntries);
@ -89,31 +90,39 @@ export class Room extends EventEmitter {
* Used for decrypting when loading/filling the timeline, and retrying decryption, * Used for decrypting when loading/filling the timeline, and retrying decryption,
* not during sync, where it is split up during the multiple phases. * not during sync, where it is split up during the multiple phases.
*/ */
async _decryptEntries(source, entries, inboundSessionTxn = null) { _decryptEntries(source, entries, inboundSessionTxn = null) {
if (!inboundSessionTxn) { const request = new DecryptionRequest(async r => {
inboundSessionTxn = await this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]); if (!inboundSessionTxn) {
} inboundSessionTxn = await this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]);
const events = entries.filter(entry => { }
return entry.eventType === EVENT_ENCRYPTED_TYPE; if (r.cancelled) return;
}).map(entry => entry.event); const events = entries.filter(entry => {
const isTimelineOpen = this._isTimelineOpen; return entry.eventType === EVENT_ENCRYPTED_TYPE;
const preparation = await this._roomEncryption.prepareDecryptAll(events, source, isTimelineOpen, inboundSessionTxn); }).map(entry => entry.event);
const changes = await preparation.decrypt(); const isTimelineOpen = this._isTimelineOpen;
const stores = [this._storage.storeNames.groupSessionDecryptions]; r.preparation = await this._roomEncryption.prepareDecryptAll(events, source, isTimelineOpen, inboundSessionTxn);
if (isTimelineOpen) { if (r.cancelled) return;
// read to fetch devices if timeline is open // TODO: should this throw an AbortError?
stores.push(this._storage.storeNames.deviceIdentities); const changes = await r.preparation.decrypt();
} r.preparation = null;
const writeTxn = await this._storage.readWriteTxn(stores); if (r.cancelled) return;
let decryption; const stores = [this._storage.storeNames.groupSessionDecryptions];
try { if (isTimelineOpen) {
decryption = await changes.write(writeTxn); // read to fetch devices if timeline is open
} catch (err) { stores.push(this._storage.storeNames.deviceIdentities);
writeTxn.abort(); }
throw err; const writeTxn = await this._storage.readWriteTxn(stores);
} let decryption;
await writeTxn.complete(); try {
decryption.applyToEntries(entries); decryption = await changes.write(writeTxn);
} catch (err) {
writeTxn.abort();
throw err;
}
await writeTxn.complete();
decryption.applyToEntries(entries);
});
return request;
} }
get needsPrepareSync() { get needsPrepareSync() {
@ -349,7 +358,8 @@ export class Room extends EventEmitter {
} }
await txn.complete(); await txn.complete();
if (this._roomEncryption) { if (this._roomEncryption) {
await this._decryptEntries(DecryptionSource.Timeline, gapResult.entries); const decryptRequest = this._decryptEntries(DecryptionSource.Timeline, gapResult.entries);
await decryptRequest.complete();
} }
// once txn is committed, update in-memory state & emit events // once txn is committed, update in-memory state & emit events
for (const fragment of gapResult.fragments) { for (const fragment of gapResult.fragments) {
@ -461,7 +471,7 @@ export class Room extends EventEmitter {
} }
/** @public */ /** @public */
async openTimeline() { openTimeline() {
if (this._timeline) { if (this._timeline) {
throw new Error("not dealing with load race here for now"); throw new Error("not dealing with load race here for now");
} }
@ -483,7 +493,6 @@ export class Room extends EventEmitter {
if (this._roomEncryption) { if (this._roomEncryption) {
this._timeline.enableEncryption(this._decryptEntries.bind(this, DecryptionSource.Timeline)); this._timeline.enableEncryption(this._decryptEntries.bind(this, DecryptionSource.Timeline));
} }
await this._timeline.load();
return this._timeline; return this._timeline;
} }
@ -502,3 +511,25 @@ export class Room extends EventEmitter {
} }
} }
class DecryptionRequest {
constructor(decryptFn) {
this._cancelled = false;
this.preparation = null;
this._promise = decryptFn(this);
}
complete() {
return this._promise;
}
get cancelled() {
return this._cancelled;
}
dispose() {
this._cancelled = true;
if (this.preparation) {
this.preparation.dispose();
}
}
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import {SortedArray, MappedList, ConcatList} from "../../../observable/index.js"; import {SortedArray, MappedList, ConcatList} from "../../../observable/index.js";
import {Disposables} from "../../../utils/Disposables.js";
import {Direction} from "./Direction.js"; import {Direction} from "./Direction.js";
import {TimelineReader} from "./persistence/TimelineReader.js"; import {TimelineReader} from "./persistence/TimelineReader.js";
import {PendingEventEntry} from "./entries/PendingEventEntry.js"; import {PendingEventEntry} from "./entries/PendingEventEntry.js";
@ -26,12 +27,14 @@ export class Timeline {
this._storage = storage; this._storage = storage;
this._closeCallback = closeCallback; this._closeCallback = closeCallback;
this._fragmentIdComparer = fragmentIdComparer; this._fragmentIdComparer = fragmentIdComparer;
this._disposables = new Disposables();
this._remoteEntries = new SortedArray((a, b) => a.compare(b)); this._remoteEntries = new SortedArray((a, b) => a.compare(b));
this._timelineReader = new TimelineReader({ this._timelineReader = new TimelineReader({
roomId: this._roomId, roomId: this._roomId,
storage: this._storage, storage: this._storage,
fragmentIdComparer: this._fragmentIdComparer fragmentIdComparer: this._fragmentIdComparer
}); });
this._readerRequest = null;
const localEntries = new MappedList(pendingEvents, pe => { const localEntries = new MappedList(pendingEvents, pe => {
return new PendingEventEntry({pendingEvent: pe, user}); return new PendingEventEntry({pendingEvent: pe, user});
}, (pee, params) => { }, (pee, params) => {
@ -42,8 +45,14 @@ export class Timeline {
/** @package */ /** @package */
async load() { async load() {
const entries = await this._timelineReader.readFromEnd(25); // 30 seems to be a good amount to fill the entire screen
this._remoteEntries.setManySorted(entries); const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(30));
try {
const entries = await readerRequest.complete();
this._remoteEntries.setManySorted(entries);
} finally {
this._disposables.disposeTracked(readerRequest);
}
} }
replaceEntries(entries) { replaceEntries(entries) {
@ -71,12 +80,17 @@ export class Timeline {
if (!firstEventEntry) { if (!firstEventEntry) {
return; return;
} }
const entries = await this._timelineReader.readFrom( const readerRequest = this._disposables.track(this._timelineReader.readFrom(
firstEventEntry.asEventKey(), firstEventEntry.asEventKey(),
Direction.Backward, Direction.Backward,
amount amount
); ));
this._remoteEntries.setManySorted(entries); try {
const entries = await readerRequest.complete();
this._remoteEntries.setManySorted(entries);
} finally {
this._disposables.disposeTracked(readerRequest);
}
} }
/** @public */ /** @public */
@ -87,6 +101,8 @@ export class Timeline {
/** @public */ /** @public */
close() { close() {
if (this._closeCallback) { if (this._closeCallback) {
this._readerRequest?.dispose();
this._readerRequest = null;
this._closeCallback(); this._closeCallback();
this._closeCallback = null; this._closeCallback = null;
} }

View file

@ -19,6 +19,24 @@ import {Direction} from "../Direction.js";
import {EventEntry} from "../entries/EventEntry.js"; import {EventEntry} from "../entries/EventEntry.js";
import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js"; import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js";
class ReaderRequest {
constructor(fn) {
this.decryptRequest = null;
this._promise = fn(this);
}
complete() {
return this._promise;
}
dispose() {
if (this.decryptRequest) {
this.decryptRequest.dispose();
this.decryptRequest = null;
}
}
}
export class TimelineReader { export class TimelineReader {
constructor({roomId, storage, fragmentIdComparer}) { constructor({roomId, storage, fragmentIdComparer}) {
this._roomId = roomId; this._roomId = roomId;
@ -42,12 +60,33 @@ export class TimelineReader {
return this._storage.readTxn(stores); return this._storage.readTxn(stores);
} }
async readFrom(eventKey, direction, amount) { readFrom(eventKey, direction, amount) {
const txn = await this._openTxn(); return new ReaderRequest(async r => {
return await this._readFrom(eventKey, direction, amount, txn); const txn = await this._openTxn();
return await this._readFrom(eventKey, direction, amount, r, txn);
});
} }
async _readFrom(eventKey, direction, amount, txn) { readFromEnd(amount) {
return new ReaderRequest(async r => {
const txn = await this._openTxn();
const liveFragment = await txn.timelineFragments.liveFragment(this._roomId);
let entries;
// room hasn't been synced yet
if (!liveFragment) {
entries = [];
} else {
this._fragmentIdComparer.add(liveFragment);
const liveFragmentEntry = FragmentBoundaryEntry.end(liveFragment, this._fragmentIdComparer);
const eventKey = liveFragmentEntry.asEventKey();
entries = await this._readFrom(eventKey, Direction.Backward, amount, r, txn);
entries.unshift(liveFragmentEntry);
}
return entries;
});
}
async _readFrom(eventKey, direction, amount, r, txn) {
let entries = []; let entries = [];
const timelineStore = txn.timelineEvents; const timelineStore = txn.timelineEvents;
const fragmentStore = txn.timelineFragments; const fragmentStore = txn.timelineFragments;
@ -83,25 +122,12 @@ export class TimelineReader {
} }
if (this._decryptEntries) { if (this._decryptEntries) {
await this._decryptEntries(entries, txn); r.decryptRequest = this._decryptEntries(entries, txn);
} try {
await r.decryptRequest.complete();
return entries; } finally {
} r.decryptRequest = null;
}
async readFromEnd(amount) {
const txn = await this._openTxn();
const liveFragment = await txn.timelineFragments.liveFragment(this._roomId);
let entries;
// room hasn't been synced yet
if (!liveFragment) {
entries = [];
} else {
this._fragmentIdComparer.add(liveFragment);
const liveFragmentEntry = FragmentBoundaryEntry.end(liveFragment, this._fragmentIdComparer);
const eventKey = liveFragmentEntry.asEventKey();
entries = await this._readFrom(eventKey, Direction.Backward, amount, txn);
entries.unshift(liveFragmentEntry);
} }
return entries; return entries;
} }

View file

@ -29,6 +29,7 @@ export class Disposables {
track(disposable) { track(disposable) {
this._disposables.push(disposable); this._disposables.push(disposable);
return disposable;
} }
dispose() { dispose() {

View file

@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// polyfills needed for IE11 // polyfills needed for IE11
// just enough to run olm, have promises and async/await
import "regenerator-runtime/runtime"; import "regenerator-runtime/runtime";
import "core-js/modules/es.promise"; import "core-js/modules/es.promise";
import "core-js/modules/es.math.imul";
import "core-js/modules/es.math.clz32";

View file

@ -94,13 +94,13 @@ class MessageHandler {
} }
async _handleMessage(message) { async _handleMessage(message) {
switch (message.type) { const {type} = message;
case "load_olm": if (type === "ping") {
this._sendReply(message, await this._loadOlm(message.path)); this._sendReply(message, {type: "pong"});
break; } else if (type === "load_olm") {
case "megolm_decrypt": this._sendReply(message, await this._loadOlm(message.path));
this._sendReply(message, this._megolmDecrypt(message.sessionKey, message.ciphertext)); } else if (type === "megolm_decrypt") {
break; this._sendReply(message, this._megolmDecrypt(message.sessionKey, message.ciphertext));
} }
} }
} }