Skip to content

Add LRU cache for generated parsers to FnInvisibleXml - #2749

Merged
ChristianGruen merged 2 commits into
BaseXdb:mainfrom
GuntherRademacher:ixml-cache
Sep 1, 2026
Merged

Add LRU cache for generated parsers to FnInvisibleXml#2749
ChristianGruen merged 2 commits into
BaseXdb:mainfrom
GuntherRademacher:ixml-cache

Conversation

@GuntherRademacher

Copy link
Copy Markdown
Member

This PR adds an LRU cache for Markup-Blitz-generated parsers to FnInvisibleXml (generated parsers are stateless and safe to share concurrently). The cache is limited to a fixed size of 15.

The XQuery code below can be used to demonstrate the performance difference that is gained by adding the cache. It repeatedly parses a number of small documents with a JSON grammar (returning the count of result nodes). It runs about 20 times faster with the cache.

JSON parser cache benchmark
declare variable $n external := 100;

declare variable $grammar := ``[        -json: ws, -value, ws.
        value: map;
               array;
               number;
               string;
               boolean;
               null.
      boolean: 'false';
               'true'.
         null: -'null'.
          map: -'{', ws, (member, ws)**(-',', ws), -'}'.
       member: key, ws, -':', ws, value.
          key: -string.
        array: -'[', ws, (-value, ws)**(-',', ws), -']'.
       number: '-'?, int, frac?, exp?.
    -digit1-9: ['1'-'9'].
           -e: ['eE'].
         -exp: e, ['-+']?, DIGIT+.
        -frac: '.', DIGIT+.
         -int: '0';
               digit1-9, DIGIT*.
       string: -'"', char*, -'"'.
        -char: ~['"\'; #9; #a; #d];
               -'\', escaped.
     -escaped: '"';
               '\';
               '/';
               -'b', +#8;
               -'f', +#c;
               -'n', +#a;
               -'r', +#d;
               -'t', +#9;
               unicode.
      unicode: -'u', code.
        @code: HEXDIG, HEXDIG, HEXDIG, HEXDIG.
       -DIGIT: ['0'-'9'].
      -HEXDIG: DIGIT;
               ['a'-'f'; 'A'-'F'].
          -ws: -[' '; #9; #a; #d]*.
]``;

declare variable $documents := (
  '{"id":1,"ok":true,"tag":"a"}',
  '{"id":2,"ok":false,"tag":"bb"}',
  '{"n":3,"name":"alfa","v":0}',
  '{"n":4,"name":"bravo","v":12}',
  '{"city":"Bonn","temp":21.5}',
  '{"city":"Kiel","rain":false}',
  '{"a":[1,2,3],"b":"short"}',
  '{"a":[true,false],"b":null}',
  '{"user":"ana","roles":["r"]}',
  '{"user":"bert","roles":["r","w"]}',
  '{"x":-17,"y":42,"unit":"m"}',
  '{"x":0.125,"y":-4,"unit":"m"}',
  '{"code":"A-17","active":true}',
  '{"code":"B-29","active":false}',
  '{"k":"plain","items":[1,1,2]}',
  '{"k":"quote","text":"hello"}',
  '{"path":"/a/b","depth":2}',
  '{"path":"/alpha/beta","depth":2}',
  '{"hex":"00ff","bytes":[0,255]}',
  '{"hex":"cafe","bytes":[202,254]}',
  '{"date":"2026-08-29","z":0}',
  '{"date":"2025-01-01","z":1}',
  '{"flag":true,"score":99.75}',
  '{"flag":false,"score":12.5}',
  '{"meta":{"a":1},"list":[2,3]}',
  '{"meta":{"b":2},"list":[4,5]}',
  '{"s":"escape\\nline","ok":true}',
  '{"s":"slash\\/path","ok":true}',
  '{"u":"\\u0041","name":"unicode"}',
  '{"u":"\\u20ac","name":"euro"}',
  '{"mix":[null,true,3,"x"]}',
  '{"mix":[false,null,4,"yy"]}',
  '{"title":"Small JSON sample","count":7}',
  '{"title":"Another compact sample","count":8}',
  '{"bounds":{"min":1,"max":9},"closed":true}',
  '{"bounds":{"min":-2,"max":11},"closed":false}',
  '{"points":[{"x":1,"y":2},{"x":3,"y":4}]}',
  '{"points":[{"x":-1,"y":0},{"x":5,"y":8}]}',
  '{"catalog":"a","ids":[101,102,103],"valid":true}',
  '{"catalog":"b","ids":[201,202,203],"valid":false}',
  '{"message":"this is still a tiny json document"}',
  '{"message":"small payloads make parser setup visible"}',
  '{"array":[10,20,30,40,50],"label":"five"}',
  '{"array":[5,4,3,2,1],"label":"reverse"}',
  '{"nested":{"left":{"v":1},"right":{"v":2}}}',
  '{"nested":{"left":{"v":3},"right":{"v":4}}}',
  '{"name":"delta","tags":["red","green"],"rank":4}',
  '{"name":"echo","tags":["blue","gray"],"rank":5}',
  '{"config":{"retry":3,"delay":1.25},"enabled":true}',
  '{"config":{"retry":5,"delay":0.75},"enabled":false}',
  '{"records":[{"id":1},{"id":2},{"id":3}],"page":1}',
  '{"records":[{"id":4},{"id":5},{"id":6}],"page":2}',
  '{"text":"abcdefghijklmnopqrstuvwxyz","len":26}',
  '{"text":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","len":26}',
  '{"route":["start","middle","end"],"cost":14}',
  '{"route":["north","east","south"],"cost":21}',
  '{"sensor":"alpha-01","values":[0.1,0.2,0.3]}',
  '{"sensor":"beta-02","values":[9.8,9.7,9.6]}',
  '{"long":"012345678901234567890123456789","ok":true}',
  '{"long":"abcdefghijklmnopqrstuvwxyzzzzzzzz","ok":false}',
  '{"obj":{"a":[1,2],"b":{"c":"d"}},"tail":null}',
  '{"obj":{"a":[3,4],"b":{"c":"e"}},"tail":true}',
  '{"final":64,"payload":{"name":"last","items":[1,2,3]}}',
  '{"final":65,"payload":{"name":"extra","items":[4,5,6]}}'
);

declare function local:parse($input as xs:string) {
  invisible-xml($grammar, { 'fail-on-error': true() })($input)//*
};

prof:time(
    (1 to $n)
    ! $documents
    =!> local:parse()
    => count()
    => sum()
)

@ChristianGruen
ChristianGruen merged commit ba732ad into BaseXdb:main Sep 1, 2026
1 check passed
@ChristianGruen
ChristianGruen deleted the ixml-cache branch September 1, 2026 18:54
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