Skip to content

Commit 3323702

Browse files
authored
Rework client router filter handling (#49741)
This simplifies the implementation for the client router filter and makes the error rate configuring more accurate. Additional tests haven't been added as this is still a matter of probability and existing tests cover the client router behavior. Example scenario of filter size/false positives with an error rate of `0.01%`: ```sh entries: 128 filter size: 4986 bytes, gzip size: 627 bytes false positives with 100000 samples: 26 0.026% ``` x-ref: [slack thread](https://vercel.slack.com/archives/C04K237UHCP/p1683129488543039)
1 parent c3f54ec commit 3323702

File tree

12 files changed

+106
-883
lines changed

12 files changed

+106
-883
lines changed

packages/next/src/lib/create-client-router-filter.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-
55
import { Redirect } from './load-custom-routes'
66
import { tryToParsePath } from './try-to-parse-path'
77

8-
const POTENTIAL_ERROR_RATE = 0.01
9-
108
export function createClientRouterFilter(
119
paths: string[],
1210
redirects: Redirect[],
13-
allowedErrorRate: number = POTENTIAL_ERROR_RATE
11+
allowedErrorRate?: number
1412
): {
1513
staticFilter: ReturnType<BloomFilter['export']>
1614
dynamicFilter: ReturnType<BloomFilter['export']>

packages/next/src/server/lib/incremental-cache/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
IncrementalCacheValue,
99
IncrementalCacheEntry,
1010
} from '../../response-cache'
11-
import { encode } from '../../../shared/lib/bloom-filter/base64-arraybuffer'
11+
import { encode } from '../../../shared/lib/base64-arraybuffer'
1212
import { encodeText } from '../../stream-utils/encode-decode'
1313
import {
1414
CACHE_ONE_YEAR,

packages/next/src/server/lib/patch-fetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ export function patchFetch({
446446

447447
if (process.env.NEXT_RUNTIME === 'edge') {
448448
const { decode } =
449-
require('../../shared/lib/bloom-filter/base64-arraybuffer') as typeof import('../../shared/lib/bloom-filter/base64-arraybuffer')
449+
require('../../shared/lib/base64-arraybuffer') as typeof import('../../shared/lib/base64-arraybuffer')
450450
decodedBody = decode(resData.body)
451451
} else {
452452
decodedBody = Buffer.from(resData.body, 'base64').subarray()

packages/next/src/shared/lib/bloom-filter/base64-arraybuffer.ts renamed to packages/next/src/shared/lib/base64-arraybuffer.ts

File renamed without changes.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// minimal implementation MurmurHash2 hash function
2+
function murmurhash2(str: string) {
3+
let h = 0
4+
for (let i = 0; i < str.length; i++) {
5+
const c = str.charCodeAt(i)
6+
h = Math.imul(h ^ c, 0x5bd1e995)
7+
h ^= h >>> 13
8+
h = Math.imul(h, 0x5bd1e995)
9+
}
10+
return h >>> 0
11+
}
12+
13+
export class BloomFilter {
14+
numItems: number
15+
errorRate: number
16+
numBits: number
17+
numHashes: number
18+
bitArray: number[]
19+
20+
constructor(numItems: number, errorRate: number) {
21+
this.numItems = numItems
22+
this.errorRate = errorRate
23+
this.numBits = Math.ceil(
24+
-(numItems * Math.log(errorRate)) / (Math.log(2) * Math.log(2))
25+
)
26+
this.numHashes = Math.ceil((this.numBits / numItems) * Math.log(2))
27+
this.bitArray = new Array(this.numBits).fill(0)
28+
}
29+
30+
static from(items: string[], errorRate = 0.01) {
31+
const filter = new BloomFilter(items.length, errorRate)
32+
33+
for (const item of items) {
34+
filter.add(item)
35+
}
36+
return filter
37+
}
38+
39+
export() {
40+
const data = {
41+
numItems: this.numItems,
42+
errorRate: this.errorRate,
43+
numBits: this.numBits,
44+
numHashes: this.numHashes,
45+
bitArray: this.bitArray,
46+
}
47+
48+
if (typeof window === 'undefined' && process.env.NEXT_RUNTIME !== 'edge') {
49+
if (this.errorRate < 0.01) {
50+
const filterData = JSON.stringify(data)
51+
const gzipSize = require('next/dist/compiled/gzip-size').sync(
52+
filterData
53+
)
54+
55+
if (gzipSize > 1024) {
56+
console.warn(
57+
`Creating filter with error rate less than 1% (0.01) can increase the size dramatically proceed with caution. Received error rate ${this.errorRate} resulted in size ${filterData.length} bytes, ${gzipSize} bytes (gzip)`
58+
)
59+
}
60+
}
61+
}
62+
63+
return data
64+
}
65+
66+
import(data: ReturnType<typeof this['export']>) {
67+
this.numItems = data.numItems
68+
this.errorRate = data.errorRate
69+
this.numBits = data.numBits
70+
this.numHashes = data.numHashes
71+
this.bitArray = data.bitArray
72+
}
73+
74+
add(item: string) {
75+
const hashValues = this.getHashValues(item)
76+
hashValues.forEach((hash) => {
77+
this.bitArray[hash] = 1
78+
})
79+
}
80+
81+
contains(item: string) {
82+
const hashValues = this.getHashValues(item)
83+
return hashValues.every((hash) => this.bitArray[hash])
84+
}
85+
86+
getHashValues(item: string) {
87+
const hashValues = []
88+
for (let i = 1; i <= this.numHashes; i++) {
89+
const hash = murmurhash2(`${item}${i}`) % this.numBits
90+
hashValues.push(hash)
91+
}
92+
return hashValues
93+
}
94+
}

packages/next/src/shared/lib/bloom-filter/base-filter.ts

Lines changed: 0 additions & 119 deletions
This file was deleted.

packages/next/src/shared/lib/bloom-filter/bit-set.ts

Lines changed: 0 additions & 149 deletions
This file was deleted.

0 commit comments

Comments
 (0)