abhinav-phi opened a new pull request, #2111:
URL: https://github.com/apache/stormcrawler/pull/2111

   Fixes #2107
   
   ## What happens
   
   `Metadata.addValue(key, value)` is a copy-on-append write path: it allocates 
a new array of size *n + 1* and copies the existing *n* values on every call.
   
   `CommaSeparatedToMultivaluedMetadata.filter` removes the key, splits the 
value on commas and then calls `addValue` once per token, so splitting *n* 
tokens costs n²/2 element copies and the same order of transient allocation. 
The key was just removed on the line above, so the whole token set could be 
stored with one `setValues` call instead.
   
   `Metadata.addValues(String, String[])` has the same shape: when the key 
already exists it loops over `addValue`.
   
   ## Where
   
   - 
`core/src/main/java/org/apache/stormcrawler/parse/filter/CommaSeparatedToMultivaluedMetadata.java`
 — the per-token append loop in `filter`.
   - `core/src/main/java/org/apache/stormcrawler/Metadata.java` — 
`addValue(String, String)` (the copy-on-append write path) and 
`addValues(String, String[])` (loops over `addValue` when the key exists).
   
   ## Why it matters
   
   The archetype wires this filter onto page content by default: 
`jsoupfilters.json` maps `parse.keywords` to 
`//META[@name="keywords"]/@content`, and `parsefilters.json` runs this filter 
on `parse.keywords`. So the token list comes from the crawled page, not from 
trusted configuration.
   
   At the archetype's 65536 byte `http.content.limit`, the filter costs 
hundreds of milliseconds per page on a comma-heavy value (measured below), 
which is well inside Storm's default 30 second tuple timeout but is still a 
large multiple of normal parse cost, paid again on every replay. Because the 
cost is quadratic in the number of tokens, raising `http.content.limit` — which 
is routine — multiplies it by the square of the increase, and the library 
default of `-1` has no bound at all.
   
   ## What this PR changes
   
   1. **Single bulk write in the filter.** 
`CommaSeparatedToMultivaluedMetadata.filter` now stores all tokens with a 
single `setValues` call. Blank tokens (e.g. between consecutive commas) are 
still dropped, exactly as `Metadata.addValue` did, so the stored result is 
identical for every input.
   
   2. **Bounded token count.** The number of tokens taken from one value is 
capped by a new optional `maxTokens` filter parameter and a warning is logged 
when the cap trims, so that a page cannot decide how much metadata it produces. 
The default of 65536 is above what a page at the archetype's default 65536 byte 
content limit can produce (~32k tokens), while still bounding metadata size 
when operators raise `http.content.limit` or leave it unbounded; it can be 
lowered per deployment in `parsefilters.json`:
   
       ```json
       {
         "class": 
"org.apache.stormcrawler.parse.filter.CommaSeparatedToMultivaluedMetadata",
         "name": "CommaSeparatedToMultivaluedMetadata",
         "params": {
           "keys": ["parse.keywords"],
           "maxTokens": 1024
         }
       }
       ```
   
   3. **Single-copy append in `Metadata.addValues(String, String[])`.** When 
the key already exists it previously looped over `addValue`, i.e. one 
whole-array copy per value. It now builds one array and copies each element 
once, so the quadratic pattern cannot come back through another caller. As a 
side effect it no longer skips blank values in that case — which matches what 
it already did when the key was absent, so the method is now self-consistent. 
`addValue(String, String)` itself is unchanged and keeps skipping blank values.
   
   ## Performance
   
   The reproduction test from the issue (`timeAtArchetypeContentLimit`: 32768 
tokens, 65536 chars, the archetype's content limit), run on Windows, JDK 25, in 
a fresh surefire JVM, before and after:
   
   - **Before:** 1223 ms (the issue measured 162 ms on faster hardware; the 
figure is dominated by ~5·10⁸ element copies either way).
   - **After:** 272 ms under identical cold-JVM conditions, and ~30 ms once the 
JVM is warm — the remaining cost is the regex split and one linear pass over 
the tokens.
   
   More important than the constant factor is the shape: the cost now grows 
linearly with the number of tokens instead of quadratically, so raising 
`http.content.limit` no longer multiplies the filter cost by the square of the 
increase.
   
   ## Tests
   
   New 
`core/src/test/java/org/apache/stormcrawler/parse/filter/CommaSeparatedToMultivaluedMetadataTest.java`:
   
   - `splittingUsesABulkAppend` counts calls to the copy-on-append path through 
a `Metadata` subclass; it fails on main (1000 calls to `addValue` for 1000 
tokens) and passes with this change.
   - `timeAtArchetypeContentLimit` prints the cost at the archetype content 
limit and asserts all 32768 tokens survive.
   - `splittingHandlesBlankTokensAndCap` pins down the behaviour of the fix: 
blank tokens are still dropped, an all-blank value leaves no entry behind, and 
values beyond `maxTokens` are trimmed.
   
   Verified locally:
   
   - `mvn -pl core test` — 417 tests, 0 failures, 0 errors.
   - `mvn checkstyle:check test-compile` over the whole reactor — clean.
   
   ## Note for reviewers
   
   Unrelated to this change: running the full build locally, the editorconfig 
and RAT checks currently flag two pre-existing files on main 
(`external/opensearch/dashboards` and 
`external/solr/archetype/src/main/resources/archetype-resources/configsets` — 
missing final newline, no license header). Both are untouched by this PR.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to