medikoo / memoizee

Complete memoize/cache solution for JavaScript
ISC License
1.73k stars 61 forks source link

Callback for cache access #118

Closed ThomWright closed 3 years ago

ThomWright commented 3 years ago

I'm trying to add Prometheus metrics around my cache use. The metrics I'd like are:

Hit rate can then be hits / accesses, and eviction rate evictions / accesses.

This is what I have now;

// ... init metrics counters

return memoizee(myFunction, {
  max: 250,
  dispose() {
    evictionCounter.inc()
  },
})

Having another callback, in addition to dispose which gets called after every access and has (hit: boolean) as the parameter would be really useful.

For example:

function onAccess(hit: boolean) {
  accessCounter.inc()
  if (hit) {
    hitCounter.inc()
  }
}
medikoo commented 3 years ago

@ThomWright have you considered relying on memoizee/profile ?

ThomWright commented 3 years ago

@ThomWright have you considered relying on memoizee/profile ?

Blimey that was quick :smile:

I saw it, but also saw:

Mind also that running profile module affects performance, it's best not to use it in production environment

I would like to run this in a production environment :wink:

medikoo commented 3 years ago

@ThomWright I think main part that affects performance in profiler is generation of the stack traces (so profile stats show were memoized function was invoked)

I think there are two ways you can avoid that

  1. (requires changes to memoizee). Introduce an extra option for profilre, to provide more basic statistics, without stack traces, and also we need to update it, so gathered data is accessible programmatically (currently only log function is exposd which returns human readable summary)
  2. You may prepare you're own profiler, by creating your own memoizee extension, as it's the only way to attach to set and get events as here: https://github.com/medikoo/memoizee/blob/cd7cc2738c183fd00f7e8bf5ba6a96935f2b3dab/profile.js#L37-L38 And afaik it's all you need to gather needed data. To attach to those events you need to access conf object, which is not expose on memoized function, but is exposed to eventual extensions. While unfortunately there's no perfect documentation, on how to create extension, you may take an inspiration on how extension can be configured by inspecting those prepared here: https://github.com/medikoo/memoizee/tree/cd7cc2738c183fd00f7e8bf5ba6a96935f2b3dab/ext
ThomWright commented 3 years ago

Great, thank you. I'll have a look at the extensions.

ThomWright commented 3 years ago

That solution seems to work, thanks. For reference, here is what I did:

const memoizeeExtensions = require("memoizee/lib/registered-extensions")
memoizeeExtensions.metrics = function(
  _: unknown,
  conf: EventEmitter,
  options: memoizee.Options<Parse>,
) {
  const postfix =
    (options.async && memoizeeExtensions.async) ||
    (options.promise && memoizeeExtensions.promise)
      ? "async"
      : ""

  conf.on("set" + postfix, (id: string) => {
    queryCounter.inc()
  })
  conf.on("get" + postfix, (id: string) => {
    queryCounter.inc()
    hitsCounter.inc()
  })
  conf.on("delete" + postfix, (id: string) => {
    evictionCounter.inc()
  })
}

return memoizee(parse, {
  primitive: true,
  max: 250,
  metrics: true,
})

To get the types to work I also did this:

// typings/memoizee/index.d.ts
import "memoizee"

declare module "memoizee" {
  export interface Options<F extends (...args: any[]) => any> {
    // To turn on our own extension
    metrics?: boolean
  }
}