From e2e1bb90caf00dcac93b896625999191073da932 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 11 Sep 2023 15:33:16 +0100 Subject: [PATCH] Make query optimizer pluggable --- packages/app/src/index.tsx | 20 +++++++- packages/system-query/pkg/system_query.d.ts | 6 +++ packages/system-query/pkg/system_query.js | 20 ++++++++ .../system-query/pkg/system_query_bg.wasm | Bin 118793 -> 120018 bytes .../pkg/system_query_bg.wasm.d.ts | 1 + packages/system-query/src/lib.rs | 7 +++ packages/system/src/gossip-model.ts | 2 +- packages/system/src/index.ts | 17 ++++++- packages/system/src/nostr-system.ts | 28 +++++++++--- packages/system/src/query-optimizer/index.ts | 43 ++++++++++++++++++ .../{ => query-optimizer}/request-expander.ts | 27 ++--------- .../{ => query-optimizer}/request-merger.ts | 4 +- .../{ => query-optimizer}/request-splitter.ts | 16 ++----- packages/system/src/request-builder.ts | 27 +++++------ packages/system/src/system-worker.ts | 9 ++++ packages/system/src/utils.ts | 2 +- 16 files changed, 164 insertions(+), 65 deletions(-) create mode 100644 packages/system/src/query-optimizer/index.ts rename packages/system/src/{ => query-optimizer}/request-expander.ts (64%) rename packages/system/src/{ => query-optimizer}/request-merger.ts (97%) rename packages/system/src/{ => query-optimizer}/request-splitter.ts (67%) diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index 7bce8257f..8435f23cf 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -2,14 +2,14 @@ import "./index.css"; import "@szhsin/react-menu/dist/index.css"; import "./fonts/inter.css"; -import {default as wasmInit} from "@snort/system-query"; +import { compress, expand_filter, flat_merge, get_diff, default as wasmInit } from "@snort/system-query"; import WasmPath from "@snort/system-query/pkg/system_query_bg.wasm"; import { StrictMode } from "react"; import * as ReactDOM from "react-dom/client"; import { Provider } from "react-redux"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; -import { EventPublisher, NostrSystem, ProfileLoaderService, Nip7Signer, PowWorker } from "@snort/system"; +import { EventPublisher, NostrSystem, ProfileLoaderService, Nip7Signer, PowWorker, QueryOptimizer, FlatReqFilter, ReqFilter } from "@snort/system"; import { SnortContext } from "@snort/system-react"; import * as serviceWorkerRegistration from "serviceWorkerRegistration"; @@ -39,6 +39,21 @@ import { db } from "Db"; import { preload, RelayMetrics, UserCache, UserRelays } from "Cache"; import { LoginStore } from "Login"; +const WasmQueryOptimizer = { + expandFilter: (f: ReqFilter) => { + return expand_filter(f) as Array; + }, + getDiff: (prev: Array, next: Array) => { + return get_diff(prev, next) as Array; + }, + flatMerge: (all: Array) => { + return flat_merge(all) as Array; + }, + compress: (all: Array) => { + return compress(all) as Array; + } +} as QueryOptimizer; + /** * Singleton nostr system */ @@ -46,6 +61,7 @@ export const System = new NostrSystem({ relayCache: UserRelays, profileCache: UserCache, relayMetrics: RelayMetrics, + queryOptimizer: WasmQueryOptimizer, authHandler: async (c, r) => { const { publicKey, privateKey } = LoginStore.snapshot(); if (privateKey) { diff --git a/packages/system-query/pkg/system_query.d.ts b/packages/system-query/pkg/system_query.d.ts index 4b24908a0..4fdcb2a92 100644 --- a/packages/system-query/pkg/system_query.d.ts +++ b/packages/system-query/pkg/system_query.d.ts @@ -22,6 +22,11 @@ export function get_diff(prev: any, next: any): any; * @returns {any} */ export function flat_merge(val: any): any; +/** +* @param {any} val +* @returns {any} +*/ +export function compress(val: any): any; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; @@ -31,6 +36,7 @@ export interface InitOutput { readonly expand_filter: (a: number, b: number) => void; readonly get_diff: (a: number, b: number, c: number) => void; readonly flat_merge: (a: number, b: number) => void; + readonly compress: (a: number, b: number) => void; readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; diff --git a/packages/system-query/pkg/system_query.js b/packages/system-query/pkg/system_query.js index 9fb7deef2..57dd87cf8 100644 --- a/packages/system-query/pkg/system_query.js +++ b/packages/system-query/pkg/system_query.js @@ -270,6 +270,26 @@ export function flat_merge(val) { } } +/** +* @param {any} val +* @returns {any} +*/ +export function compress(val) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.compress(retptr, addHeapObject(val)); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + function handleError(f, args) { try { return f.apply(this, args); diff --git a/packages/system-query/pkg/system_query_bg.wasm b/packages/system-query/pkg/system_query_bg.wasm index 394d0f79785d2ed823f835286cfe364280ea8c71..8590da75f1d85807cab185e994d2f223b8f054d5 100644 GIT binary patch delta 24748 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 delta 23730 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 Result { Ok(serde_wasm_bindgen::to_value(&result)?) } +#[wasm_bindgen] +pub fn compress(val: JsValue) -> Result { + let val_parsed: Vec = serde_wasm_bindgen::from_value(val)?; + let result = merge::merge::(val_parsed.iter().collect()); + Ok(serde_wasm_bindgen::to_value(&result)?) +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/system/src/gossip-model.ts b/packages/system/src/gossip-model.ts index d931d6b66..2f2d9be82 100644 --- a/packages/system/src/gossip-model.ts +++ b/packages/system/src/gossip-model.ts @@ -1,7 +1,7 @@ import { ReqFilter, UsersRelays } from "."; import { dedupe, unwrap } from "@snort/shared"; import debug from "debug"; -import { FlatReqFilter } from "request-expander"; +import { FlatReqFilter } from "./query-optimizer"; const PickNRelays = 2; diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index dd1f1f1dc..9bc4dd067 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -1,9 +1,11 @@ import { AuthHandler, RelaySettings, ConnectionStateSnapshot } from "./connection"; import { RequestBuilder } from "./request-builder"; -import { NoteStore, NoteStoreHook, NoteStoreSnapshotData } from "./note-collection"; +import { NoteStore, NoteStoreSnapshotData } from "./note-collection"; import { Query } from "./query"; import { NostrEvent, ReqFilter, TaggedNostrEvent } from "./nostr"; import { ProfileLoaderService } from "./profile-cache"; +import { RelayCache } from "./gossip-model"; +import { QueryOptimizer } from "./query-optimizer"; export * from "./nostr-system"; export { default as EventKind } from "./event-kind"; @@ -24,6 +26,7 @@ export * from "./signer"; export * from "./text"; export * from "./pow"; export * from "./pow-util"; +export * from "./query-optimizer"; export * from "./impl/nip4"; export * from "./impl/nip44"; @@ -96,6 +99,16 @@ export interface SystemInterface { * Profile cache/loader */ get ProfileLoader(): ProfileLoaderService; + + /** + * Relay cache for "Gossip" model + */ + get RelayCache(): RelayCache; + + /** + * Query optimizer + */ + get QueryOptimizer(): QueryOptimizer; } export interface SystemSnapshot { @@ -121,4 +134,4 @@ export interface MessageEncryptor { getSharedSecret(privateKey: string, publicKey: string): Promise | Uint8Array; encryptData(plaintext: string, sharedSecet: Uint8Array): Promise | MessageEncryptorPayload; decryptData(payload: MessageEncryptorPayload, sharedSecet: Uint8Array): Promise | string; -} +} \ No newline at end of file diff --git a/packages/system/src/nostr-system.ts b/packages/system/src/nostr-system.ts index ec22de6c6..b7105016d 100644 --- a/packages/system/src/nostr-system.ts +++ b/packages/system/src/nostr-system.ts @@ -20,6 +20,8 @@ import { UsersRelays, } from "."; import { EventsCache } from "./cache/events"; +import { RelayCache } from "./gossip-model"; +import { QueryOptimizer, DefaultQueryOptimizer } from "./query-optimizer"; /** * Manages nostr content retrieval system @@ -72,12 +74,18 @@ export class NostrSystem extends ExternalStore implements System */ #eventsCache: FeedCache; + /** + * Query optimizer instance + */ + #queryOptimizer: QueryOptimizer; + constructor(props: { authHandler?: AuthHandler; relayCache?: FeedCache; profileCache?: FeedCache; relayMetrics?: FeedCache; eventsCache?: FeedCache; + queryOptimizer?: QueryOptimizer; }) { super(); this.#handleAuth = props.authHandler; @@ -85,6 +93,7 @@ export class NostrSystem extends ExternalStore implements System this.#profileCache = props.profileCache ?? new UserProfileCache(); this.#relayMetricsCache = props.relayMetrics ?? new RelayMetricCache(); this.#eventsCache = props.eventsCache ?? new EventsCache(); + this.#queryOptimizer = props.queryOptimizer ?? DefaultQueryOptimizer; this.#profileLoader = new ProfileLoaderService(this, this.#profileCache); this.#relayMetrics = new RelayMetricHandler(this.#relayMetricsCache); @@ -92,9 +101,6 @@ export class NostrSystem extends ExternalStore implements System } HandleAuth?: AuthHandler | undefined; - /** - * Profile loader service allows you to request profiles - */ get ProfileLoader() { return this.#profileLoader; } @@ -103,6 +109,14 @@ export class NostrSystem extends ExternalStore implements System return [...this.#sockets.values()].map(a => a.snapshot()); } + get RelayCache(): RelayCache { + return this.#relayCache; + } + + get QueryOptimizer(): QueryOptimizer { + return this.#queryOptimizer; + } + /** * Setup caches */ @@ -241,8 +255,8 @@ export class NostrSystem extends ExternalStore implements System return existing; } const filters = !req.options?.skipDiff - ? req.buildDiff(this.#relayCache, existing.filters) - : req.build(this.#relayCache); + ? req.buildDiff(this, existing.filters) + : req.build(this); if (filters.length === 0 && !!req.options?.skipDiff) { return existing; } else { @@ -255,7 +269,7 @@ export class NostrSystem extends ExternalStore implements System } else { const store = new type(); - const filters = req.build(this.#relayCache); + const filters = req.build(this); const q = new Query(req.id, req.instance, store, req.options?.leaveOpen); if (filters.some(a => a.filters.some(b => b.ids))) { const expectIds = new Set(filters.flatMap(a => a.filters).flatMap(a => a.ids ?? [])); @@ -397,4 +411,4 @@ export class NostrSystem extends ExternalStore implements System } setTimeout(() => this.#cleanup(), 1_000); } -} +} \ No newline at end of file diff --git a/packages/system/src/query-optimizer/index.ts b/packages/system/src/query-optimizer/index.ts new file mode 100644 index 000000000..eb5a166d3 --- /dev/null +++ b/packages/system/src/query-optimizer/index.ts @@ -0,0 +1,43 @@ +import { ReqFilter } from "../nostr" +import { expandFilter } from "./request-expander" +import { flatMerge, mergeSimilar } from "./request-merger" +import { diffFilters } from "./request-splitter" + +export interface FlatReqFilter { + keys: number; + ids?: string; + authors?: string; + kinds?: number; + "#e"?: string; + "#p"?: string; + "#t"?: string; + "#d"?: string; + "#r"?: string; + search?: string; + since?: number; + until?: number; + limit?: number; +} + +export interface QueryOptimizer { + expandFilter(f: ReqFilter): Array + getDiff(prev: Array, next: Array): Array + flatMerge(all: Array): Array + compress(all: Array): Array +} + +export const DefaultQueryOptimizer = { + expandFilter: (f: ReqFilter) => { + return expandFilter(f); + }, + getDiff: (prev: Array, next: Array) => { + const diff = diffFilters(prev.flatMap(a => expandFilter(a)), next.flatMap(a => expandFilter(a))); + return diff.added; + }, + flatMerge: (all: Array) => { + return flatMerge(all); + }, + compress: (all: Array) => { + return mergeSimilar(all); + } + } as QueryOptimizer; \ No newline at end of file diff --git a/packages/system/src/request-expander.ts b/packages/system/src/query-optimizer/request-expander.ts similarity index 64% rename from packages/system/src/request-expander.ts rename to packages/system/src/query-optimizer/request-expander.ts index 2efa1d2c0..3dfdb5ef7 100644 --- a/packages/system/src/request-expander.ts +++ b/packages/system/src/query-optimizer/request-expander.ts @@ -1,27 +1,11 @@ -import { ReqFilter } from "./nostr"; -import {expand_filter} from "@snort/system-query"; - -export interface FlatReqFilter { - keys: number; - ids?: string; - authors?: string; - kinds?: number; - "#e"?: string; - "#p"?: string; - "#t"?: string; - "#d"?: string; - "#r"?: string; - search?: string; - since?: number; - until?: number; - limit?: number; -} +import { FlatReqFilter } from "."; +import { ReqFilter } from "../nostr"; /** * Expand a filter into its most fine grained form */ export function expandFilter(f: ReqFilter): Array { - /*const ret: Array = []; + const ret: Array = []; const src = Object.entries(f); const keys = src.filter(([, v]) => Array.isArray(v)).map(a => a[0]); const props = src.filter(([, v]) => !Array.isArray(v)); @@ -47,8 +31,5 @@ export function expandFilter(f: ReqFilter): Array { ...Object.fromEntries(props), }); - return ret;*/ - - const ret = expand_filter(f); - return ret as Array; + return ret; } diff --git a/packages/system/src/request-merger.ts b/packages/system/src/query-optimizer/request-merger.ts similarity index 97% rename from packages/system/src/request-merger.ts rename to packages/system/src/query-optimizer/request-merger.ts index 7b2f5cac1..5ed3900e1 100644 --- a/packages/system/src/request-merger.ts +++ b/packages/system/src/query-optimizer/request-merger.ts @@ -1,6 +1,6 @@ import { distance } from "@snort/shared"; -import { ReqFilter } from "."; -import { FlatReqFilter } from "./request-expander"; +import { ReqFilter } from ".."; +import { FlatReqFilter } from "."; /** * Keys which can change the entire meaning of the filter outside the array types diff --git a/packages/system/src/request-splitter.ts b/packages/system/src/query-optimizer/request-splitter.ts similarity index 67% rename from packages/system/src/request-splitter.ts rename to packages/system/src/query-optimizer/request-splitter.ts index 931458d42..97f9b6284 100644 --- a/packages/system/src/request-splitter.ts +++ b/packages/system/src/query-optimizer/request-splitter.ts @@ -1,9 +1,8 @@ -import { flatFilterEq } from "./utils"; -import { FlatReqFilter } from "./request-expander"; -import { diff_filters } from "@snort/system-query"; +import { flatFilterEq } from "../utils"; +import { FlatReqFilter } from "."; export function diffFilters(prev: Array, next: Array, calcRemoved?: boolean) { - /*const added = []; + const added = []; const removed = []; for (const n of next) { @@ -29,12 +28,5 @@ export function diffFilters(prev: Array, next: Array 0, - added: (added as Array), - removed: [] - } + }; } \ No newline at end of file diff --git a/packages/system/src/request-builder.ts b/packages/system/src/request-builder.ts index 9a7c49aad..dcd0ba668 100644 --- a/packages/system/src/request-builder.ts +++ b/packages/system/src/request-builder.ts @@ -1,12 +1,11 @@ import debug from "debug"; import { v4 as uuid } from "uuid"; import { appendDedupe, sanitizeRelayUrl, unixNowMs } from "@snort/shared"; -import { flat_merge, get_diff }from "@snort/system-query"; -import { ReqFilter, u256, HexKey, EventKind } from "."; +import EventKind from "./event-kind"; +import { SystemInterface } from "index"; +import { ReqFilter, u256, HexKey } from "./nostr"; import { RelayCache, splitByWriteRelays, splitFlatByWriteRelays } from "./gossip-model"; -import { flatMerge, mergeSimilar } from "./request-merger"; -import { FlatReqFilter } from "./request-expander"; /** * Which strategy is used when building REQ filters @@ -95,27 +94,25 @@ export class RequestBuilder { return this.#builders.map(f => f.filter); } - build(relays: RelayCache): Array { - const expanded = this.#builders.flatMap(a => a.build(relays, this.id)); - return this.#groupByRelay(expanded); + build(system: SystemInterface): Array { + const expanded = this.#builders.flatMap(a => a.build(system.RelayCache, this.id)); + return this.#groupByRelay(system, expanded); } /** * Detects a change in request from a previous set of filters */ - buildDiff(relays: RelayCache, prev: Array): Array { + buildDiff(system: SystemInterface, prev: Array): Array { const start = unixNowMs(); - //const next = this.#builders.flatMap(f => expandFilter(f.filter)); - //const diff = diffFilters(prev, next); - const diff = get_diff(prev, this.buildRaw()) as Array; + const diff = system.QueryOptimizer.getDiff(prev, this.buildRaw()); const ts = unixNowMs() - start; this.#log("buildDiff %s %d ms", this.id, ts); if (diff.length > 0) { - return splitFlatByWriteRelays(relays, diff).map(a => { + return splitFlatByWriteRelays(system.RelayCache, diff).map(a => { return { strategy: RequestStrategy.AuthorsRelays, - filters: flat_merge(a.filters) as Array, + filters: system.QueryOptimizer.flatMerge(a.filters), relay: a.relay, }; }); @@ -130,7 +127,7 @@ export class RequestBuilder { * @param expanded * @returns */ - #groupByRelay(expanded: Array) { + #groupByRelay(system: SystemInterface, expanded: Array) { const relayMerged = expanded.reduce((acc, v) => { const existing = acc.get(v.relay); if (existing) { @@ -143,7 +140,7 @@ export class RequestBuilder { const filtersSquashed = [...relayMerged.values()].map(a => { return { - filters: mergeSimilar(a.flatMap(b => b.filters)), + filters: system.QueryOptimizer.compress(a.flatMap(b => b.filters)), relay: a[0].relay, strategy: a[0].strategy, } as BuiltRawReqFilter; diff --git a/packages/system/src/system-worker.ts b/packages/system/src/system-worker.ts index 622f03dd6..5871cc031 100644 --- a/packages/system/src/system-worker.ts +++ b/packages/system/src/system-worker.ts @@ -6,6 +6,8 @@ import { NostrEvent, TaggedNostrEvent } from "./nostr"; import { NoteStore, NoteStoreSnapshotData } from "./note-collection"; import { Query } from "./query"; import { RequestBuilder } from "./request-builder"; +import { RelayCache } from "./gossip-model"; +import { QueryOptimizer } from "./query-optimizer"; export class SystemWorker extends ExternalStore implements SystemInterface { #port: MessagePort; @@ -29,6 +31,13 @@ export class SystemWorker extends ExternalStore implements Syste throw new Error("Method not implemented."); } + get RelayCache(): RelayCache { + throw new Error("Method not implemented."); + } + + get QueryOptimizer(): QueryOptimizer { + throw new Error("Method not implemented."); + } HandleAuth?: AuthHandler; get Sockets(): ConnectionStateSnapshot[] { diff --git a/packages/system/src/utils.ts b/packages/system/src/utils.ts index ec9a2b353..319e30c00 100644 --- a/packages/system/src/utils.ts +++ b/packages/system/src/utils.ts @@ -1,5 +1,5 @@ import { equalProp } from "@snort/shared"; -import { FlatReqFilter } from "./request-expander"; +import { FlatReqFilter } from "./query-optimizer"; import { NostrEvent, ReqFilter } from "./nostr"; export function findTag(e: NostrEvent, tag: string) {