Skip to content

Commit 7261597

Browse files
committed
Performance improvements
1 parent 27957d1 commit 7261597

File tree

13 files changed

+190
-192
lines changed

13 files changed

+190
-192
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
## 0.16.0
2+
3+
**Breaking change, databases created using earlier versions is not compatible with 0.16.0 and up.**
4+
5+
- Use 32 bit MurmurHash3 (from new dependency `npm:ohash` instead of SHA-1 for transaction integrity check,
6+
saving time and storage
7+
- Precalculate the Uint32Array of the transaction signature, improving
8+
performance
9+
- Use CBOR-encoding of key instead of custom implementation, improving
10+
performance
11+
- Avoid copying arraybuffers in certain situations
12+
- Prefetch data on transaction read, severely reducing the number of disk reads
13+
114
## 0.15.11
215

316
- Remove option doLock from `.sync()`

deno.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cross/kv",
3-
"version": "0.15.11",
3+
"version": "0.16.0",
44
"exports": {
55
".": "./mod.ts",
66
"./cli": "./src/cli/mod.ts"
@@ -12,7 +12,8 @@
1212
"@cross/utils": "jsr:@cross/utils@^0.13.0",
1313
"@std/assert": "jsr:@std/assert@^0.226.0",
1414
"@std/path": "jsr:@std/path@^0.225.1",
15-
"cbor-x": "npm:cbor-x@^1.5.9"
15+
"cbor-x": "npm:cbor-x@^1.5.9",
16+
"ohash": "npm:ohash@^1.1.3"
1617
},
1718
"publish": {
1819
"exclude": [".github", "test/*"]

src/lib/cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { LEDGER_CACHE_MEMORY_FACTOR } from "./constants.ts";
22
import type { KVLedgerResult } from "./ledger.ts";
33

44
/**
5-
* An in-memory cache for `KVLedgerResult` objects, optimized for append-only ledgers.
5+
* An in-memory cache for `KVLedgerResult` objects.
66
*
77
* This cache stores transaction results (`KVLedgerResult`) associated with their offsets within the ledger.
88
* It maintains a fixed maximum size and evicts the oldest entries (Least Recently Used - LRU)

src/lib/constants.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22
export const LOCK_DEFAULT_MAX_RETRIES = 32;
33
export const LOCK_DEFAULT_INITIAL_RETRY_INTERVAL_MS = 30; // Increased with itself on each retry, so the actual retry interval is 20, 40, 60 etc. 32 and 20 become about 10 seconds total.
44
export const LOCK_STALE_TIMEOUT_MS = 6 * 60 * 60 * 1000; // Automatically unlock a ledger that has been locked for more than 2*60*60*1000 = 2 hours.
5-
export const LEDGER_CURRENT_VERSION: string = "B015";
5+
export const LEDGER_CURRENT_VERSION: string = "B016";
66
export const SUPPORTED_LEDGER_VERSIONS: string[] = [
77
LEDGER_CURRENT_VERSION,
8-
"B011",
9-
"B012",
10-
"B013",
118
];
9+
export const LEDGER_PREFETCH_BYTES = 50 * 1024; // Prefetch chunks of 50KB of data while reading the ledger
1210
export const LEDGER_MAX_READ_FAILURES = 10;
13-
export const LEDGER_PREFETCH_BYTES = 256;
1411
export const SYNC_INTERVAL_MS = 2_500; // Overridable with instance configuration
1512
export const LEDGER_CACHE_MB = 100; // Allow 100 MBytes of the ledger to exist in RAM. Not an exact science due to LEDGER_CACHE_MEMORY_FACTOR.
1613
export const LEDGER_CACHE_MEMORY_FACTOR = 3; // Assume that ledger entries take about n times as much space when unwrapped in RAM. Used for ledger cache memory limit, does not need to be exakt.
@@ -21,5 +18,7 @@ export const LOCKED_BYTES_LENGTH = 8; // Length of timestamp
2118
export const LOCK_BYTE_OFFSET = LEDGER_BASE_OFFSET - LOCKED_BYTES_LENGTH; // Last 8 bytes of the header
2219
export const KV_KEY_ALLOWED_CHARS = /^[@\p{L}\p{N}_-]+$/u; // Unicode letters and numbers, undescore, hyphen and at
2320
export const LEDGER_FILE_ID: string = "CKVD"; // Cross/KV Database
24-
export const TRANSACTION_SIGNATURE: string = "T;"; // Cross/Kv Transaction
21+
export const ENCODED_TRANSACTION_SIGNATURE: Uint8Array = new TextEncoder()
22+
.encode("T;"); // Cross/Kv Transaction
2523
export const UNLOCKED_BYTES = new Uint8Array(LOCKED_BYTES_LENGTH);
24+
export const LOCKED_BYTES = new Uint8Array(LOCKED_BYTES_LENGTH);

src/lib/key.ts

Lines changed: 13 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { decode, encode } from "cbor-x";
12
import { KV_KEY_ALLOWED_CHARS } from "./constants.ts";
23

34
// Helper function to stringify range values correctly
@@ -68,11 +69,11 @@ export class KVKeyInstance {
6869
private isQuery: boolean;
6970
public byteLength?: number;
7071
constructor(
71-
key: KVQuery | KVKey | DataView,
72+
key: KVQuery | KVKey | Uint8Array,
7273
isQuery: boolean = false,
7374
validate: boolean = true,
7475
) {
75-
if (key instanceof DataView) {
76+
if (key instanceof Uint8Array) {
7677
this.key = this.fromUint8Array(key);
7778
} else {
7879
this.key = key;
@@ -87,48 +88,8 @@ export class KVKeyInstance {
8788
* Encodes the key elements into a byte array suitable for storage in a transaction header.
8889
*/
8990
public toUint8Array(): Uint8Array {
90-
const keyBytesArray = [];
91-
for (const element of this.key) {
92-
if (typeof element === "string") {
93-
keyBytesArray.push(new Uint8Array([0])); // Type: String
94-
const strBytes = new TextEncoder().encode(element);
95-
const strLengthBytes = new Uint8Array(4);
96-
new DataView(strLengthBytes.buffer).setUint32(
97-
0,
98-
strBytes.length,
99-
false,
100-
);
101-
keyBytesArray.push(strLengthBytes);
102-
keyBytesArray.push(strBytes);
103-
} else if (typeof element === "number") {
104-
keyBytesArray.push(new Uint8Array([1])); // Type: Number
105-
const numBytes = new Uint8Array(8);
106-
new DataView(numBytes.buffer).setFloat64(0, element, false);
107-
keyBytesArray.push(numBytes);
108-
} else {
109-
// This should never happen if validate() is working correctly
110-
throw new TypeError("Invalid key element type");
111-
}
112-
}
113-
114-
// Encode the number of key elements
115-
const numKeyElementsBytes = new Uint8Array(1);
116-
new DataView(numKeyElementsBytes.buffer).setUint8(
117-
0,
118-
this.key.length,
119-
);
120-
keyBytesArray.unshift(numKeyElementsBytes); // Add to the beginning
121-
122-
const keyArray = new Uint8Array(
123-
keyBytesArray.reduce((a, b) => a + b.length, 0),
124-
);
125-
let keyOffset = 0;
126-
for (const bytes of keyBytesArray) {
127-
keyArray.set(bytes, keyOffset);
128-
keyOffset += bytes.length;
129-
}
130-
131-
return keyArray;
91+
const data = encode(this.key);
92+
return new Uint8Array(data, 0, data.byteLength);
13293
}
13394

13495
/**
@@ -137,44 +98,9 @@ export class KVKeyInstance {
13798
* @param data - The byte array containing the encoded key.
13899
* @throws {Error} If the key cannot be decoded.
139100
*/
140-
private fromUint8Array(dataView: DataView): KVKey {
141-
let offset = 0;
142-
143-
// 1. Decode Number of Key Elements (uint32)
144-
const numKeyElements = dataView.getUint8(offset);
145-
offset += 1;
146-
147-
const keyToBe: KVKey = [];
148-
149-
for (let i = 0; i < numKeyElements; i++) {
150-
// 2. Decode Element Type (uint8): 0 for string, 1 for number
151-
const elementType = dataView.getUint8(offset);
152-
offset += 1;
153-
154-
if (elementType === 0) { // String
155-
// 3a. Decode String Length (uint32)
156-
const strLength = dataView.getUint32(offset, false);
157-
offset += 4;
158-
159-
// 3b. Decode String Bytes
160-
const strBytes = new DataView(
161-
dataView.buffer,
162-
dataView.byteOffset + offset,
163-
strLength,
164-
);
165-
keyToBe.push(new TextDecoder().decode(strBytes));
166-
offset += strLength;
167-
} else if (elementType === 1) { // Number
168-
// 3c. Decode Number (float64)
169-
const numValue = dataView.getFloat64(offset, false);
170-
keyToBe.push(numValue);
171-
offset += 8;
172-
} else {
173-
throw new Error(`Invalid key element type ${elementType}`);
174-
}
175-
}
176-
this.byteLength = offset;
177-
return keyToBe;
101+
private fromUint8Array(data: Uint8Array): KVKey {
102+
this.key = decode(data);
103+
return this.key as KVKey;
178104
}
179105

180106
get(): KVQuery | KVKey {
@@ -301,7 +227,7 @@ export class KVKeyInstance {
301227
}
302228
}
303229

304-
const instance = new KVKeyInstance(result, isQuery);
230+
const instance = new KVKeyInstance(result, isQuery, false);
305231
instance.validate();
306232

307233
return isQuery ? result : result as KVKey;
@@ -380,7 +306,10 @@ export class KVKeyInstance {
380306
const subquery = query.slice(i + 1);
381307
const subkey = thisKey.slice(i + 1);
382308
if (
383-
!new KVKeyInstance(subkey, true).matchesQuery(subquery, recursive)
309+
!new KVKeyInstance(subkey, true, false).matchesQuery(
310+
subquery,
311+
recursive,
312+
)
384313
) {
385314
return false;
386315
}

src/lib/kv.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export class KV extends EventEmitter {
159159
"Invalid option: syncIntervalMs must be a positive integer",
160160
);
161161
}
162-
this.syncIntervalMs = options.syncIntervalMs ?? SYNC_INTERVAL_MS;
162+
this.syncIntervalMs = options.syncIntervalMs ?? this.syncIntervalMs;
163163
// - ledgerCacheSize
164164
if (
165165
options.ledgerCacheSize !== undefined &&
@@ -209,9 +209,9 @@ export class KV extends EventEmitter {
209209
this.ledger = new KVLedger(filePath, this.ledgerCacheSize);
210210
this.ledgerPath = filePath;
211211
await this.ledger.open(createIfMissing);
212-
213212
// Do the initial synchronization
214213
// - If `this.autoSync` is enabled, additional synchronizations will be carried out every `this.syncIntervalMs`
214+
215215
const syncResult = await this.sync();
216216
if (syncResult.error) {
217217
throw syncResult.error;
@@ -747,8 +747,12 @@ export class KV extends EventEmitter {
747747
this.ensureOpen();
748748
this.ensureIndex();
749749

750+
const validatedQuery: KVKeyInstance | null = key === null
751+
? null
752+
: new KVKeyInstance(key, true);
753+
750754
return this.index.getChildKeys(
751-
key === null ? null : new KVKeyInstance(key, true),
755+
key === null ? null : validatedQuery,
752756
);
753757
}
754758

0 commit comments

Comments
 (0)