]> git.openstreetmap.org Git - rails.git/commit
Keep linkification regexps in memory
authorPablo Brasero <pablo@pablobm.com>
Wed, 24 Jun 2026 12:23:42 +0000 (13:23 +0100)
committerPablo Brasero <pablo@pablobm.com>
Wed, 24 Jun 2026 22:39:09 +0000 (23:39 +0100)
commitd4d49bc0628698b65d73801c58e68df16f249253
tree9c8ed6b68f0f04eb5b57f30495d4dee38668de5f
parent5053e2e76b82031b54cba71944d50128929459b7
Keep linkification regexps in memory

I was playing with [derailed_benchmarks](https://github.com/zombocom/derailed_benchmarks) today and noticed that history pages were spending a long time in `Regexp#initialize`. Example:

```
$ PATH_TO_HIT=/node/29/history be derailed exec perf:stackprof
Booting: production
(...)
Method: GET
Endpoint: "/node/29/history"
(...)
==================================
  Mode: cpu(1000)
  Samples: 3716 (15.58% miss rate)
  GC: 343 (9.23%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
       529  (14.2%)         529  (14.2%)     Regexp#initialize
       230   (6.2%)         230   (6.2%)     (marking)
       165   (4.4%)         165   (4.4%)     Dir.glob
       163   (4.4%)         163   (4.4%)     Zlib::Inflate#inflate
       113   (3.0%)         113   (3.0%)     (sweeping)
        88   (2.4%)          78   (2.1%)     String#gsub
(...)
```

Note that this is measured while running the app in production mode.

I went poking and saw that the [18 patterns (as of these lines)](https://github.com/openstreetmap/openstreetmap-website/blob/5053e2e76b82031b54cba71944d50128929459b7/config/settings.yml#L146-L182) used to detect text to linkify are being used to instantiate `Regexp` every time that linkification takes place. Looks like this takes a significant amount of time.

This PR moves the instantiation to a class method where the result is memoized. This is the result, for comparison:

```
$ PATH_TO_HIT=/node/29/history be derailed exec perf:stackprof
Booting: production
(...)
Method: GET
Endpoint: "/node/29/history"
(...)
==================================
  Mode: cpu(1000)
  Samples: 3521 (19.50% miss rate)
  GC: 329 (9.34%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
       226   (6.4%)         226   (6.4%)     (marking)
       184   (5.2%)         184   (5.2%)     Zlib::Inflate#inflate
       131   (3.7%)         119   (3.4%)     String#gsub
       102   (2.9%)         102   (2.9%)     (sweeping)
        79   (2.2%)          67   (1.9%)     Set#initialize
        60   (1.7%)          60   (1.7%)     PG::Connection#exec_prepared
```

Or in terms of requests per second, this is the code currently in master:

```
$ PATH_TO_HIT=/node/29/history bundle exec derailed exec perf:ips
Booting: production
(...)
Database 'openstreetmap' already exists
Method: GET
Endpoint: "/node/29/history"
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [x86_64-linux]
Warming up --------------------------------------
                 ips     1.000 i/100ms
Calculating -------------------------------------
                 ips     23.713 (±25.3%) i/s   (42.17 ms/i) -    119.000 in   5.018360s
```

And this is this PR:

```
> PATH_TO_HIT=/node/29/history bundle exec derailed exec perf:ips
Booting: production
(...)
Method: GET
Endpoint: "/node/29/history"
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [x86_64-linux]
Warming up --------------------------------------
                 ips     1.000 i/100ms
Calculating -------------------------------------
                 ips     30.870 (±25.9%) i/s   (32.39 ms/i) -    155.000 in   5.021047s
```

So that's from ~24 ips to ~30 ips, or 25% more requests per second.

Caveat: the tests for `RichText` set up custom patterns, assuming no
memoization. To work around this, I have added a `RichText.reset_state`
method that is run before each relevant test.
lib/rich_text.rb
test/lib/rich_text_test.rb