Skip to content

Commit

Permalink
Automatically calculate ideal size of cache pool
Browse files Browse the repository at this point in the history
I tested a few variations and this was the winner for overall performance
  • Loading branch information
nfriedly committed Aug 24, 2023
1 parent 0feaeeb commit 821000b
Showing 1 changed file with 43 additions and 36 deletions.
79 changes: 43 additions & 36 deletions source/memory-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ type Client = {
totalHits: number
resetTime: Date
}

const average = (array: number[]) =>
array.reduce((a, b) => a + b) / array.length

/**
* A `Store` that stores the hit count for each client in memory.
*
Expand All @@ -23,22 +27,30 @@ export default class MemoryStore implements Store {
* These two maps store usage (requests) and reset time by key (IP)
*
* They are split into two to avoid having to iterate through the entire set to determine which ones need reset.
* Instead, Clients are moved from previous to current as new requests come in from them.
* Once windowMS has elapsed, all clients left in previous are reset at once.
* Instead, Clients are moved from previous to current as new requests come in.
* Once windowMS has elapsed, all clients left in previous are known to be expired.
* At that point, the cache pool is filled from previous, and any remaining clients are cleared.
*
*/
previous!: Map<string, Client>
current!: Map<string, Client>
previous = new Map<string, Client>()
current = new Map<string, Client>()

/**
* Pool of unused clients, kept to reduce the number of objects created and destroyed
* Cache of unused clients, kept to reduce the number of objects created and destroyed.
*
* Improves performance, at the expense of a small amount of RAM.
*
* Each individual entry takes up 152 bytes.
* In one benchmark, the total time taken to handle 100M requests was reduced from 70.184s to 47.862s (~32% improvement) with ~5.022 MB extra RAM used.
* .
*/
pool!: Client[]
pool: Client[] = []

/**
* Maximum number of unused clients to keep in the pool
* Used to calculate how many entries to keep in the pool
*/
poolSize = 100
newClients = 0
recentNew: number[] = []

/**
* Reference to the active timer.
Expand All @@ -51,20 +63,6 @@ export default class MemoryStore implements Store {
*/
localKeys = true

/**
* Create a new MemoryStore with an optional custom poolSize
*
* Note that the windowMS option is passed to init() by express-rate-limit
*
* @param [options]
* @param [options.poolSize] - Maximum number of unused objects to keep around. Increase to reduce garbage collection.
*/
constructor({ poolSize }: { poolSize?: number } = {}) {
if (typeof poolSize === 'number') {
this.poolSize = poolSize
}
}

/**
* Method that initializes the store.
*
Expand All @@ -74,13 +72,6 @@ export default class MemoryStore implements Store {
// Get the duration of a window from the options.
this.windowMs = options.windowMs

// Initialise the hit counter map
this.previous = new Map()
this.current = new Map()

// Initialize the spare client pool
this.pool = []

// Indicates that init was called more than once.
// Could happen if a store was shared between multiple instances.
if (this.interval) {
Expand All @@ -89,7 +80,7 @@ export default class MemoryStore implements Store {

// Reset all clients left in previous every `windowMs`.
this.interval = setInterval(() => {
this.resetPrevious()
this.clearExpired()
}, this.windowMs)

// Cleaning up the interval will be taken care of by the `shutdown` method.
Expand Down Expand Up @@ -172,11 +163,20 @@ export default class MemoryStore implements Store {
/**
* Refill the pool, set previous to current, reset current
*/
private resetPrevious() {
const temporary = this.previous
this.previous = this.current
let poolSpace = this.poolSize - this.pool.length
for (const client of temporary.values()) {
private clearExpired() {
// At this point, all clients in previous are expired

// calculate the target pool size
this.recentNew.push(this.newClients)
if (this.recentNew.length > 10) this.recentNew.shift()
this.newClients = 0
const targetPoolSize = Math.round(average(this.recentNew))

// Calculate how many entries to potentially copy to the pool
let poolSpace = targetPoolSize - this.pool.length

// Fill up the pool with expired clients
for (const client of this.previous.values()) {
if (poolSpace > 0) {
this.pool.push(client)
poolSpace--
Expand All @@ -185,7 +185,12 @@ export default class MemoryStore implements Store {
}
}

temporary.clear()
// Clear all expired clients from previous
this.previous.clear()

// Swap previous and temporary
const temporary = this.previous
this.previous = this.current
this.current = temporary
}

Expand All @@ -205,9 +210,11 @@ export default class MemoryStore implements Store {
} else if (this.pool.length > 0) {
client = this.pool.pop()!
this.resetClient(client)
this.newClients++
} else {
client = { totalHits: 0, resetTime: new Date() }
this.resetClient(client)
this.newClients++
}

this.current.set(key, client)
Expand Down

0 comments on commit 821000b

Please sign in to comment.