Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(helper/cookie): fast-path for name specified #3608

Merged
merged 6 commits into from
Nov 2, 2024

Conversation

exoego
Copy link
Contributor

@exoego exoego commented Nov 1, 2024

I find myself using getCookie(c, 'name') a lot, so I want to make it faster.
In this PR,

  • Array.reduce is replaced with plain for-of loop, which is a bit faster and allows early return
  • Two early returns (fast-paths) are added for the cases where name is specified

Overall, the optimized version is faster in all three cases

  • When parse-all cookie, a bit faster (1-2%).
  • When the name is specified and
    • found in cookie, several times faster (depending on cookie length and key position, though)
    • not found in cookie, a lot more faster

The author should do the following, if applicable

  • Add tests
  • Run tests
  • bun run format:fix && bun run lint:fix to format the code
  • Add TSDoc/JSDoc to document the code

Benchmark

benchmark code
import { bench, run } from 'mitata'

const decodeURIComponent_ = decodeURIComponent
const validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/
const validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/

const baseParse = (cookie, name) => {
  const pairs = cookie.trim().split(';')
  return pairs.reduce((parsedCookie, pairStr) => {
    pairStr = pairStr.trim()
    const valueStartPos = pairStr.indexOf('=')
    if (valueStartPos === -1) {
      return parsedCookie
    }

    const cookieName = pairStr.substring(0, valueStartPos).trim()
    if ((name && name !== cookieName) || !validCookieNameRegEx.test(cookieName)) {
      return parsedCookie
    }

    let cookieValue = pairStr.substring(valueStartPos + 1).trim()
    if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) {
      cookieValue = cookieValue.slice(1, -1)
    }
    if (validCookieValueRegEx.test(cookieValue)) {
      parsedCookie[cookieName] = decodeURIComponent_(cookieValue)
    }

    return parsedCookie
  }, {})
}

export const optimizedParse = (cookie, name) => {
  if (name && cookie.indexOf(name) === -1) {
    // Fast-path: return the single-key object of the provided name
    return {}
  }
  const pairs = cookie.trim().split(';')
  const parsedCookie = {}
  for (const _pairStr of pairs) {
    const pairStr = _pairStr.trim()
    const valueStartPos = pairStr.indexOf('=')
    if (valueStartPos === -1) {
      continue
    }

    const cookieName = pairStr.substring(0, valueStartPos).trim()
    if ((name && name !== cookieName) || !validCookieNameRegEx.test(cookieName)) {
      continue
    }

    let cookieValue = pairStr.substring(valueStartPos + 1).trim()
    if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) {
      cookieValue = cookieValue.slice(1, -1)
    }
    if (validCookieValueRegEx.test(cookieValue)) {
      parsedCookie[cookieName] = decodeURIComponent_(cookieValue)
      if (name) {
        // Fast-path: return the single-key object of the provided name
        break
      }
    }
  }
  return parsedCookie
}

const cookieString = 'yummy_cookie=chocolate; tasty_cookie=strawberry; great_cookie=oreo; bad_cookie=raisin; ok_cookie=vanilla; ugly_cookie=peanut; nice_cookie=macadamia; japanese_cookie=matcha; chinese_cookie=red bean; korean_cookie=rice cake; american_cookie=chocolate chip; italian_cookie=cannoli; french_cookie=macaron; german_cookie=lebkuchen;'

bench('base all', () => {
  const s = baseParse(cookieString)
})
bench('base name found', () => {
  const s = baseParse(cookieString, 'yummy_cookie')
})
bench('base name not found', () => {
  const s = baseParse(cookieString, 'no_such_cookie')
})
bench('opt all', () => {
  const s = optimizedParse(cookieString)
})
bench('opt name found', () => {
  const s = optimizedParse(cookieString, 'yummy_cookie')
})
bench('opt name not found', () => {
  const s = optimizedParse(cookieString, 'no_such_cookie')
})

await run()
runtime: node 22.4.0 (arm64-darwin)

benchmark              avg (min … max) p75   p99    (min … top 1%)
-------------------------------------- -------------------------------
base all                  3.87 µs/iter   3.93 µs   ▄   █▄   █      ▄  
                   (3.76 µs … 4.00 µs)   4.00 µs █▅██████▅▁▅█▅▅▅▁█▅█▅▁
base name found         811.98 ns/iter 825.36 ns         █▂           
               (758.14 ns … 872.27 ns) 865.90 ns ▂▂▁▁▂▂▂███▄▄▅▅▃▅▃▂▂▁▁
base name not found     596.51 ns/iter 607.47 ns        ▃█            
               (547.59 ns … 668.62 ns) 651.10 ns ▁▂▂▁▂▁▁███▃▅█▄▂▂▂▂▁▁▁
opt all                   3.85 µs/iter   3.89 µs     ▄▄▄  ▄ █         
                   (3.73 µs … 4.00 µs)   3.99 µs █▅█▁███▁█████▅▅▅█▁▅█▁
opt name found          309.37 ns/iter 309.96 ns █▄                   
               (303.07 ns … 376.55 ns) 354.06 ns ██▆▄▃▂▂▁▁▁▁▁▁▁▂▁▁▁▁▁▁
opt name not found      115.38 ns/iter 115.02 ns █                    
               (113.52 ns … 159.12 ns) 134.62 ns ██▃▂▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁
runtime: bun 1.1.33 (arm64-darwin)

benchmark              avg (min … max) p75   p99    (min … top 1%)
-------------------------------------- -------------------------------
base all                  4.03 µs/iter   4.08 µs  ▅█                  
                   (3.82 µs … 4.52 µs)   4.50 µs ▄██▄▆▁▁▁▃▁▁▁▁▁▁▁▁▃▆▄▃
base name found           1.36 µs/iter   1.37 µs         █            
                   (1.16 µs … 1.60 µs)   1.57 µs ▁▁▁▁▂▁▁▆██▆▃▃▂▁▁▁▁▂▃▂
base name not found       1.08 µs/iter   1.11 µs          █           
                 (956.42 ns … 1.18 µs)   1.17 µs ▁▁▁▁▁▁▂▁▅██▅▄▄▃▆▆▄▂▁▁
opt all                   3.94 µs/iter   3.87 µs     █                
                   (3.62 µs … 4.51 µs)   4.51 µs ▂▁▁▂█▆▃▁▁▁▁▂▁▁▁▁▁▁▄▃▁
opt name found          293.67 ns/iter 290.44 ns █▇                   
               (273.91 ns … 695.67 ns) 659.94 ns ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
opt name not found      143.82 ns/iter 145.19 ns ▅█                   
               (140.40 ns … 211.33 ns) 158.55 ns ███▄▃▃▄▃▃▃▂▂▂▁▁▁▁▁▁▁▁
runtime: deno 2.0.4 (aarch64-apple-darwin)

benchmark              avg (min … max) p75   p99    (min … top 1%)
-------------------------------------- -------------------------------
base all                  3.77 µs/iter   3.80 µs     █  ▂▅▂           
                   (3.67 µs … 3.91 µs)   3.90 µs ▇▄▇▄█▇▇███▄▄▇▄▄▄▇▄▁▁▄
base name found         822.18 ns/iter 825.56 ns        █▇            
               (773.38 ns … 905.11 ns) 888.21 ns ▂▂▂▁▁▁▂███▄▃▃▁▂▂▁▁▁▂▁
base name not found     618.17 ns/iter 627.11 ns        ▃█▂           
               (553.50 ns … 736.12 ns) 692.33 ns ▁▂▂▁▁▁▁███▆▅▄▃▃▂▂▁▁▂▁
opt all                   3.68 µs/iter   3.69 µs  ▅▂▂█▂▂ ▂            
                   (3.64 µs … 3.78 µs)   3.76 µs ▄██████▇█▇▁▄▄▇▄▄▁▇▁▁▁
opt name found          303.38 ns/iter 307.35 ns  █                   
               (293.99 ns … 405.34 ns) 341.54 ns ▆█▆▄▄▆▅▂▃▂▁▂▂▁▁▂▁▁▁▁▁
opt name not found      115.66 ns/iter 115.95 ns █                    
               (113.24 ns … 147.32 ns) 134.09 ns ██▃▃▃▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁

@exoego exoego changed the title perf(helper/cookie): fast-path for a specific key perf(helper/cookie): fast-path for key specified Nov 1, 2024
Copy link

codecov bot commented Nov 1, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 94.71%. Comparing base (ae99d86) to head (f2f32f1).
Report is 9 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3608   +/-   ##
=======================================
  Coverage   94.71%   94.71%           
=======================================
  Files         158      158           
  Lines        9553     9560    +7     
  Branches     2797     2784   -13     
=======================================
+ Hits         9048     9055    +7     
  Misses        505      505           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@exoego exoego marked this pull request as ready for review November 1, 2024 03:40
@exoego exoego changed the title perf(helper/cookie): fast-path for key specified perf(helper/cookie): fast-path for name specified Nov 2, 2024
Copy link
Member

@yusukebe yusukebe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@yusukebe
Copy link
Member

yusukebe commented Nov 2, 2024

@exoego

Thanks! Merging.

@yusukebe yusukebe merged commit e43f203 into honojs:main Nov 2, 2024
16 checks passed
@exoego exoego deleted the optimize-getCookie-name branch November 2, 2024 23:51
TinsFox pushed a commit to TinsFox/hono that referenced this pull request Nov 11, 2024
* perf(helper/cookie): fast-path for a specific key

* perf(helper/cookie): fast-path for a specific key not found

* perf(helper/cookie): added test for missing case

* perf(helper/cookie): fix tests

* perf(helper/cookie): fix tests

* perf(helper/cookie): cleanup tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants