Skip to content

Improve caching to reduce delay between request and XForms processing #1239

Closed

Description

@ebruchez

With a form created by a customer, we see about 1000 ms between the logging of the incoming request and the beginning of XForms processing.

Out of this we can see:

  • 220 ms: timing of reading form.xhtml
  • 243 ms: "reading input", which probably does not include the above 220 ms

Some thoughts:

  • XPL processing might take time, but why so long?
  • there should be a conditional GET on the form definition and the processing should be very fast

Activity

  1. ebruchez commented on Jan 16, 2014

    @ebruchez
    CollaboratorAuthor

    How things work now:

    • Form Runner is hooked up in an XPL pipeline
    • new/edit/view page uses model detail-model.xpl and view html-view.xpl
    • pipeline
      • model: read-form.xpl
        • oxf:url-generator
        • XInclude on form definition
        • store in request as fr-form-definition for Summary page
      • view: unroll-form.xpl
        • components.xsl
        • XInclude
      • epilogue.xpl
        • xforms-epilogue.xpl
          • oxf:xforms-to-xhtml ⇒ where static state caching takes place

    Caching at the XForms level:

    • see XFormsToSomething
    • cached against processor input
      • Stage1CacheableState (URIReferences)
      • Stage2CacheableState (staticStateDigest + template)
    • XFormsStaticState stored into separate XFormsStaticStateCache, indexed by staticStateDigest

    Here is what we expect:

    • XFormsToSomething actual dependencies
      • form definition from persistence layer
        • app name, form name (in URL path)
        • version headers, one of Orbeon-For-Document-Id or Orbeon-Form-Definition-Version
        • NOTE: document id is also passed as document URL parameter.
      • dependent URLs (static instances, schemas, XIncludes)
      • local property files
    • static state must be cached based on those dependencies
    • form definition can also change independently in db (e.g. remote publish) ⇒ ETag

    The mechanism must also work with the form version number headers:

    • requires using Vary header
    • use ETag and If-None-Match
    • what ETag could be
      • persistence layer provides it
      • digest of the form definition
      • row id of form definition in database
    • form version made available in the pipeline (which is input to caching too)
      • form-version can be provided as URL parameter to FR page
      • BUT if document id is provided, form-version is ignored and overridden by version returned by persistence layer via orbeon-form-definition-version (or 1 if not found)
    • server can respond with
      • ETag
      • Last-Modified and no Etag
      • none of those
      • Q: should persistence layer be required to have versioning capability?

    High-level algorithm proposal:

    • cache keeps entries indexed by app/form name, then ETag
    • probably only keep max of n ETags per app/form name, per LRU
    • incoming request processing
      • request to persistence layer
        • URL w/ app/form name
        • Orbeon-For-Document-Id or Orbeon-Form-Definition-Version header (vary)
        • list of ETags: all ETags in cache for given incoming app/form name
        • must keep ref during request to persistence layer to all cache entries for app/form, so they don't go away
      • response is EITHER 200 + document + ETag
        • create static state
        • cache static state by form/app name then etag
      • OR 304 + one of the ETags which matched (should be only 1)
        • refresh cache entry for app/name/etag
    • persistence implemenatation
      • gets app/form/version headers
      • determines version when needed
      • checks incoming ETags if any
      • responds with 200 + document + Etag or 304 + Etag
      • ETag could be if of document in db (would fail to update if somebody update form def in place, but nobody should!)
    • Q: should cache hold digest, or static state itself (and keep separate static state cache like now)?
      • A: TODO
    • Q: how to handle instance/schema/XInclude dependencies
      • we create the algorithm above within e.g. oxf:form-runner-persistence-client
      • produces key = ETag, validity = 0
      • oxf:xforms-to-xhtml stays connected to its input pipeline as it is now
    • if we use pipeline/custom URL generator, should be equivalent to the above

    Next steps:

    • first implementation of oxf:form-runner-persistence-client
    • make sure transformation pipeline is pure
    • document
      • what persistence implementors must do
        • either use ETags (specify what the requirements are for unicity)
        • or use Last-Modified if no versioning required
        • Q: should document URL parameter be removed?
  2. removed this from the milestone on Oct 17, 2014
  3. removed this from the milestone on Oct 17, 2014
  4. ebruchez commented on Jun 20, 2017

    @ebruchez
    CollaboratorAuthor

    Following #3272, it would be really good to improve form loading. Requirements:

    • performance
      • conditional GET
      • possibly local caching
    • native Scala
    • testable logic

    The ideas in the previous comments still appear sound. In addition, we might try to get rid of the pipeline entirely.

  5. changed the title [-]Long delay between request and XForms processing[/-] [+]Improve caching to reduce delay between request and XForms processing[/+] on Dec 7, 2017
  6. ebruchez commented on Jan 10, 2024

    @ebruchez
    CollaboratorAuthor

    +1 following discussion today.

  7. ebruchez commented on Jan 9, 2026

    @ebruchez
    CollaboratorAuthor

    Looking at the load times of a large form (~3000 controls), I notice that XSLT processing takes a very long time (~7 out of ~9 seconds). However, this XSLT processing is only there because we eventually compute a hash of the unrolled form definition. Once we obtain it, we retrieve the compiled form directly from the static state cache.

    So we get back to the idea that we should not perform a full form read/preprocessing step, but instead do a conditional GET.

    +1 from customer

  8. ebruchez commented on Jan 9, 2026

    @ebruchez
    CollaboratorAuthor

    Implementation question: where would we hook up the code? If we want to do it entirely in Scala, what would it take?

    • call persistence layer with conditional GET
      • hit: get compiled form from cache, or
      • miss: run form preprocessing including XInclude and XSLT
    • run form initialization

    Currently, this is hooked up in PFC:

    • model
      • detail-model.xpl
      • read-form.xpl
    • html-view.xpl
      • unroll-form.xpl
    • epilogue.xpl
      • epilogue-xforms.xpl

    Or, could we "simply" get read-form.xpl to return better cache key/validity, and maybe just rewrite that?

  9. ebruchez commented on Jan 10, 2026

    @ebruchez
    CollaboratorAuthor

    For reference, dependencies found for Summary page in XFormsProcessorBase:

    oxf:/apps/fr/summary/view.xhtml
    oxf:/apps/fr/includes/versioning-model.xml
    ../home-view/date-range-search.xhtml
    oxf:/apps/fr/includes/bulk-apply.xml
    oxf:/apps/fr/includes/permissions-model.xml
    

    Landing:

    oxf:/apps/fr/landing/landing.xhtml
    oxf:/apps/fr/includes/permissions-model.xml
    

    Form Builder /new

    http://localhost:9090/orbeon-forms-pe/fr/service/persistence/crud/orbeon/builder/form/form.xhtml
    oxf:/forms/orbeon/builder/form/dialog-test.xml
    oxf:/forms/orbeon/builder/form/dialog-confirmation.xml
    oxf:/forms/orbeon/builder/form/dialog-database-service.xml
    oxf:/forms/orbeon/builder/form/dialog-actions.xml
    oxf:/forms/orbeon/builder/form/dialog-schema.xml
    oxf:/forms/orbeon/builder/form/dialog-publish.xml
    oxf:/forms/orbeon/builder/form/dialog-language.xml
    oxf:/forms/orbeon/builder/form/toolbox.xml
    oxf:/apps/fr/includes/versioning-model.xml
    oxf:/forms/orbeon/builder/form/toolbox-model.xml
    oxf:/forms/orbeon/builder/form/model.xml
    oxf:/apps/fr/includes/permissions-model.xml
    
  10. ebruchez commented on Jan 10, 2026

    @ebruchez
    CollaboratorAuthor

    The "annotated-document" input of XFormsProcessorBase always gets null for both input key and validity, unsurprisingly, but verified here.

  11. ebruchez commented on Jan 12, 2026

    @ebruchez
    CollaboratorAuthor

    If we rely on the pipeline key/validity combo, here is how things should work for the form definition retrieval:

    • key: form path (which includes app/form names) + form version
    • validity: ETag
      • but do we support anything besides Long? API is Object/AnyRef)
        • in MemoryCacheImpl.lowerOrEqual(), we specifically check on Long

    Steps:

    • create native fr:form-reader (or fr:read-form) processor
      • issue conditional GET if ETag is available
    • update persistence layer to support conditional GET (if not supported yet)
    • update persistence layer to return ETag for published form definition
    • replace/update read-form.xpl
  12. 45 remaining items

  13. added 3 commits that reference this issue on Feb 5, 2026
  14. added a commit that references this issue on Feb 18, 2026
  15. added a commit that references this issue on Mar 4, 2026
  16. moved this to In progress in Orbeon Forms 2025.1.1on Mar 9, 2026
  17. ebruchez commented on Mar 23, 2026

    @ebruchez
    CollaboratorAuthor

    Check last 3 tasks above.

  18. ebruchez commented on Apr 11, 2026

    @ebruchez
    CollaboratorAuthor

    Trying to wrap my mind around the output digest question. Outputs:

    • ReadFormProcessor: params output
    • FormRunnerConfigProcessor: form-runner-config output

    In both cases, we set:

    • validity to 0L
    • key to SimpleOutputCacheKey(getProcessorClass, outputName, key)
      • where key is actually set to a digest of the output result

    On the other hand, CacheableTransformerOutputImpl "assumes that an output simply depends on all the inputs plus optional local information". The local information is enabled with supportsLocalKeyValidity. In this case, getLocalKey/getLocalValidity are called if needed. This is used by:

    • ImageServer
    • XSLTTransformer
    • DigestTransformerOutputImpl, used by:
      • DigestedProcessor, only used by PageFlowControllerProcessor
      • RequestGenerator
      • ScopeGenerator

    There must be a reason we do this?

    For ScopeGenerator, we obtain and produce the same key/validity we had set with ScopeSerializer when available.

    For ReguestGenerator, we use getFilledOutState to do the following:

    • get digest of the output from the state
    • state.key = new InternalCacheKey(getProcessor(pipelineContext), "requestHash", NumberUtils.toHexString(state.digest))
    • get OutputCacheKey from input keys and local key
      • read/write from/to ObjectCache the validity based on that local key
    if (state.validity == null) {
      state.validity = DigestTransformerOutputImpl.DEFAULT_VALIDITY // HACK so we don't recurse at the next line
    
      val outputCacheKey = getKeyImpl(pipelineContext)
      if (outputCacheKey != null) {
        val cache = ObjectCache.instance
        val digestValidity = cache.findValid(outputCacheKey, DigestTransformerOutputImpl.DEFAULT_VALIDITY).asInstanceOf[DigestValidity]
        if (digestValidity != null && util.Arrays.equals(state.digest, digestValidity.digest))
          state.validity = digestValidity.lastModified
        else {
          val currentValidity = System.currentTimeMillis: java.lang.Long
          cache.add(outputCacheKey, DigestTransformerOutputImpl.DEFAULT_VALIDITY, DigestValidity(state.digest, currentValidity))
          state.validity = currentValidity
        }
      } else
        state.validity = null // HACK restore
    }

    So the question is whether we need to use the same logic for our output keys.

  19. ebruchez commented on Apr 13, 2026

    @ebruchez
    CollaboratorAuthor

    After discussion:

    • we are not sure why the "store last modified in cache" pattern was implemented
    • it is probably ok to use 0L for those outputs
    • we could also use the last modification date of the document, but there might not be any benefit, until proven otherwise.
  20. added 2 commits that reference this issue on Apr 13, 2026
  21. ebruchez commented on Apr 15, 2026

    @ebruchez
    CollaboratorAuthor

    Some tests with a large form (~3000 static controls) and small form (Bookshelf):

    Version Large form Edit Large form New Small form Edit Small form New
    2025.1 1240 ms 1170 ms 55 ms 50 ms
    2025.1.1 350 ms 270 ms 31 ms 28 ms
  22. moved this from In progress to Done in Orbeon Forms 2025.1.1on Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions