I have tried building a general-purpose read method twice before β one common method that other methods call instead of hand-rolling their own Finds/SQL, returning data in a predictable structure. I ended up retiring both attempts. They did the job but the runtime cost was too high.
I am taking another shot at it, with a different approach. I am benchmarking the potential pieces first, one by one. I would be interested in second opinions from anyone who has built something similar, or has data that agrees or disagrees with mine.
For retrieval, I tested native find + iterate to extract data, eSQL, eDAPI, and relationship access. There was no universal winner β very different cost curves. In all cases, reads were tested while being isolated from any overhead due to session and script initialization or latency.
Native find + iterate: low entry cost, strong on small indexed reads, cost grows with the found set. Executes better server-side for small sets (N=100 -> 5ms on blank layouts against indexed fields), scales well on a slightly digressive curve (0.05 ms/record @ N=100, 0.03 ms/record @ N=10000, ~300 ms total, does even better client side).
eSQL: real fixed entry cost (200 ms server-side, 40ms client-side), scales very well (232 ms server-side @ N=10000), but hard to justify for frequent small reads.
eDAPI: very fast server-side at small and medium sets (2 ms at N=1, 4 ms at N=100, 30 ms at N=1000). Client-side it has a much higher initial cost at 80 ms, but scales on a gentler curve after that. This is a server-first technology, so that might explain that. Unfortunately, there is a deal-breaker for a general-purpose method: its returned fields depend on what is present on its target layout. I do not want a generic data-access method depending on someone editing a layout later.
Relationships: extremely fast on small sets (1 ms for one record, 11 ms for 100, indexed) but scales badly: 2700 ms at N=10000. More importantly, it introduces a graph dependency, which violates the architectural principles the system is built upon.
So Native find + iterate is the leading candidate after the architectural constraints are applied, with eSQL potentially useful for larger sets (reports, data warehousing).
I expected layout switching and variable assignment to be cheap going in β maybe not quite this cheap, but cheap. I isolated it properly: an actual layout switch measured 0.05 ms locally, 0.03 ms server-side, and switching to the current layout was a zero-cost no-op, so I see no performance reason to guard against same-layout switches. It is worth noting that this is a blank Logic layout with no objects whatsoever β I would expect a populated layout with fields/portals to behave differently once rendering cost enters the picture. Context establishment is not where my problem is, at least in this setup.
Finding the records is the cheap part, packaging them is where the battle will be lost or won. By contract, the method is guaranteed to return a JSON Object. Repeated JSONSetElement on a growing container degrades badly β 0.07 ms/element at 100, 1.05 ms/element at 1000. Best approach I found is building one multi-element JSONSetElement expression and firing it through Evaluate instead, and that held flat around 0.05 ms/element through 999 elements β 21Γ faster around the 1000 mark. I already had a brush with the 999/1000 ceiling before when using Evaluate, from a localization module that populates global variables through that function, and I have seen other community posts mention the same limit, so this was more a confirmation than a surprise. Chunking works around it and, in my opinion, chunks can safely be concatenated as text because they have already gone through JSONSetElement, so FileMaker's JSON processor has already handled escaping.
I also dug into JSONParse. One text-based JSON object read repeatedly is already fast β the automatic cache handles it. Alternate between multiple text-based objects, though, and it gets worse: server-side, I measured 0.0118 ms/read unparsed vs 0.0045 ms/read parsed. So JSONParse's value is not making one object faster β it is stopping objects from competing for the cache. This is a win for the method.
Set Variable came in around 0.005 ms including overwrites/repetitions, in line with what I expected.
Having killed two attempts on performance before, I am not assuming I have solved it this time. I would be very interested to hear from anyone who has gone down this road before. Have you centralized reads in a large solution without regretting the overhead? What mechanics did you land on? I am also curious if anyone has found a faster way to turn a large found set into structured JSON, or found a way to bypass the 1000-elements Evaluate wall.
Thank you for reading this rather long post, and for any input you might have.
Testing was done on FMS 22 / Virtualized macOS / Mac Studio M1 + FMP 22 / macOS / MacBook Air M3.