Skip to content

Commit ec0656b

Browse files
Merge pull request #1143 from deco-cx/feat/redis-compression
feat(cache): add multi-codec compression to Redis cache
2 parents 9b626b5 + 535ec23 commit ec0656b

4 files changed

Lines changed: 321 additions & 24 deletions

File tree

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "@deco/deco",
33
"version": "1.185.0",
44
"lock": false,
5+
"nodeModulesDir": "auto",
56
"exports": {
67
".": "./mod.ts",
78
"./web": "./mod.web.ts",

runtime/caches/redis.bench.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { _compress, _decompress } from "./redis.ts";
2+
3+
// Simulates a realistic cache payload: JSON with a large HTML body (5-10 KB range)
4+
// and typical response headers. Repeated to hit different size tiers.
5+
function makePayload(sizeKb: number): string {
6+
const body = `<html><body>${"<div class='product'><img src='https://cdn.bagaggio.com.br/img.jpg'/><h2>Mala de Viagem Premium</h2><p>R$ 1.299,00</p><span>Em estoque</span></div>".repeat(Math.ceil((sizeKb * 1024) / 180))}`;
7+
return JSON.stringify({
8+
body,
9+
headers: {
10+
"content-type": "text/html; charset=utf-8",
11+
"cache-control": "public, max-age=60",
12+
"x-response-time": "42ms",
13+
"vary": "Accept-Encoding",
14+
},
15+
status: 200,
16+
});
17+
}
18+
19+
const CODEC_GZIP = 0x01;
20+
const CODEC_DEFLATE = 0x02;
21+
const CODEC_LZ4 = 0x03;
22+
const CODEC_ZSTD = 0x04;
23+
24+
const payload10kb = makePayload(10);
25+
const payload100kb = makePayload(100);
26+
const payload1mb = makePayload(1000);
27+
28+
// Pre-compress for decompression benchmarks
29+
const [gz10, df10, lz10, zs10] = await Promise.all([
30+
_compress(payload10kb, CODEC_GZIP),
31+
_compress(payload10kb, CODEC_DEFLATE),
32+
_compress(payload10kb, CODEC_LZ4),
33+
_compress(payload10kb, CODEC_ZSTD),
34+
]);
35+
const [gz1mb, df1mb, lz1mb, zs1mb] = await Promise.all([
36+
_compress(payload1mb, CODEC_GZIP),
37+
_compress(payload1mb, CODEC_DEFLATE),
38+
_compress(payload1mb, CODEC_LZ4),
39+
_compress(payload1mb, CODEC_ZSTD),
40+
]);
41+
42+
// ─── Compression ratios (informational, printed once) ────────────────────────
43+
const sizes = [
44+
["10 KB", payload10kb],
45+
["100 KB", payload100kb],
46+
["1 MB", payload1mb],
47+
] as const;
48+
49+
console.log("\n── Compression ratios ──────────────────────────────────────────");
50+
for (const [label, payload] of sizes) {
51+
const raw = new TextEncoder().encode(payload).length;
52+
const results = await Promise.all([
53+
_compress(payload, CODEC_GZIP).then((c) => ({ name: "gzip ", size: c.length })),
54+
_compress(payload, CODEC_DEFLATE).then((c) => ({ name: "deflate", size: c.length })),
55+
_compress(payload, CODEC_LZ4).then((c) => ({ name: "lz4 ", size: c.length })),
56+
_compress(payload, CODEC_ZSTD).then((c) => ({ name: "zstd/1 ", size: c.length })),
57+
]);
58+
console.log(`\n${label} (raw: ${(raw / 1024).toFixed(1)} KB)`);
59+
for (const { name, size } of results) {
60+
const pct = ((1 - size / raw) * 100).toFixed(1);
61+
console.log(` ${name} ${(size / 1024).toFixed(1).padStart(7)} KB (${pct}% smaller)`);
62+
}
63+
}
64+
console.log("\n── Benchmarks ──────────────────────────────────────────────────");
65+
66+
// ─── Compress 10 KB ──────────────────────────────────────────────────────────
67+
Deno.bench({ name: "compress gzip 10KB", group: "compress-10kb" }, async () => {
68+
await _compress(payload10kb, CODEC_GZIP);
69+
});
70+
Deno.bench({ name: "compress deflate 10KB", group: "compress-10kb" }, async () => {
71+
await _compress(payload10kb, CODEC_DEFLATE);
72+
});
73+
Deno.bench({ name: "compress lz4 10KB", group: "compress-10kb" }, async () => {
74+
await _compress(payload10kb, CODEC_LZ4);
75+
});
76+
Deno.bench({ name: "compress zstd/1 10KB", group: "compress-10kb", baseline: true }, async () => {
77+
await _compress(payload10kb, CODEC_ZSTD);
78+
});
79+
80+
// ─── Compress 1 MB ───────────────────────────────────────────────────────────
81+
Deno.bench({ name: "compress gzip 1MB", group: "compress-1mb" }, async () => {
82+
await _compress(payload1mb, CODEC_GZIP);
83+
});
84+
Deno.bench({ name: "compress deflate 1MB", group: "compress-1mb" }, async () => {
85+
await _compress(payload1mb, CODEC_DEFLATE);
86+
});
87+
Deno.bench({ name: "compress lz4 1MB", group: "compress-1mb" }, async () => {
88+
await _compress(payload1mb, CODEC_LZ4);
89+
});
90+
Deno.bench({ name: "compress zstd/1 1MB", group: "compress-1mb", baseline: true }, async () => {
91+
await _compress(payload1mb, CODEC_ZSTD);
92+
});
93+
94+
// ─── Decompress 10 KB ────────────────────────────────────────────────────────
95+
Deno.bench({ name: "decompress gzip 10KB", group: "decompress-10kb" }, async () => {
96+
await _decompress(gz10);
97+
});
98+
Deno.bench({ name: "decompress deflate 10KB", group: "decompress-10kb" }, async () => {
99+
await _decompress(df10);
100+
});
101+
Deno.bench({ name: "decompress lz4 10KB", group: "decompress-10kb" }, async () => {
102+
await _decompress(lz10);
103+
});
104+
Deno.bench({ name: "decompress zstd/1 10KB", group: "decompress-10kb", baseline: true }, async () => {
105+
await _decompress(zs10);
106+
});
107+
108+
// ─── Decompress 1 MB ─────────────────────────────────────────────────────────
109+
Deno.bench({ name: "decompress gzip 1MB", group: "decompress-1mb" }, async () => {
110+
await _decompress(gz1mb);
111+
});
112+
Deno.bench({ name: "decompress deflate 1MB", group: "decompress-1mb" }, async () => {
113+
await _decompress(df1mb);
114+
});
115+
Deno.bench({ name: "decompress lz4 1MB", group: "decompress-1mb" }, async () => {
116+
await _decompress(lz1mb);
117+
});
118+
Deno.bench({ name: "decompress zstd/1 1MB", group: "decompress-1mb", baseline: true }, async () => {
119+
await _decompress(zs1mb);
120+
});

runtime/caches/redis.test.ts

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { assertEquals } from "@std/assert";
22
import {
3+
_compress,
4+
_decompress,
35
create,
46
createRevalidationLocker,
57
type RedisConnection,
@@ -13,15 +15,13 @@ Deno.test({
1315
const namespace = "test";
1416

1517
const store: RedisConnection = {
16-
get: (cacheKey: string): string => {
17-
const data: { [key: string]: string } = {
18-
a94a8fe5ccb19ba61c4c0873d391e987982fbbd3test: JSON.stringify({
19-
body: "body",
20-
status: 200,
21-
}),
18+
getBuffer: (cacheKey: string): Uint8Array | null => {
19+
const data: { [key: string]: Uint8Array } = {
20+
a94a8fe5ccb19ba61c4c0873d391e987982fbbd3test: new TextEncoder().encode(
21+
JSON.stringify({ body: "body", status: 200 }),
22+
),
2223
};
23-
24-
return data[cacheKey];
24+
return data[cacheKey] ?? null;
2525
},
2626
} as unknown as RedisConnection;
2727

@@ -50,9 +50,9 @@ Deno.test({
5050
"when the cache key takes too long to return",
5151
async () => {
5252
const timeoutStore: RedisConnection = {
53-
get: (_: string): Promise<string> =>
54-
new Promise<string>((resolve) => {
55-
setTimeout(() => resolve("{}"), 10000);
53+
getBuffer: (_: string): Promise<Uint8Array> =>
54+
new Promise<Uint8Array>((resolve) => {
55+
setTimeout(() => resolve(new TextEncoder().encode("{}")), 10000);
5656
}),
5757
} as unknown as RedisConnection;
5858

@@ -276,3 +276,55 @@ Deno.test({
276276
},
277277
);
278278
});
279+
280+
Deno.test({
281+
name: "compression round-trip",
282+
sanitizeResources: false,
283+
sanitizeOps: false,
284+
}, async (t) => {
285+
const CODEC_GZIP = 0x01;
286+
const CODEC_DEFLATE = 0x02;
287+
const CODEC_LZ4 = 0x03;
288+
const CODEC_ZSTD = 0x04;
289+
290+
const input = JSON.stringify({
291+
body: "hello world ".repeat(500),
292+
headers: { "content-type": "application/json" },
293+
status: 200,
294+
});
295+
296+
for (
297+
const [name, codec] of [
298+
["gzip", CODEC_GZIP],
299+
["deflate", CODEC_DEFLATE],
300+
["lz4", CODEC_LZ4],
301+
["zstd", CODEC_ZSTD],
302+
] as const
303+
) {
304+
await t.step(`round-trip with ${name}`, async () => {
305+
const compressed = await _compress(input, codec);
306+
assertEquals(compressed[0], codec);
307+
const decompressed = await _decompress(compressed);
308+
assertEquals(decompressed, input);
309+
});
310+
}
311+
312+
await t.step("compressed output is smaller than input", async () => {
313+
const inputBytes = new TextEncoder().encode(input).length;
314+
for (
315+
const [name, codec] of [
316+
["gzip", CODEC_GZIP],
317+
["deflate", CODEC_DEFLATE],
318+
["lz4", CODEC_LZ4],
319+
["zstd", CODEC_ZSTD],
320+
] as const
321+
) {
322+
const compressed = await _compress(input, codec);
323+
assertEquals(
324+
compressed.length < inputBytes,
325+
true,
326+
`${name}: compressed (${compressed.length}) should be smaller than input (${inputBytes})`,
327+
);
328+
}
329+
});
330+
});

0 commit comments

Comments
 (0)