Skip to content

[LoAF] Allow for annotations for long animation frame to further help debugging #3

Open

Description

@jarmit

It can be difficult to track down the source of a particular function if that function is a common function that is used multiple times within the code base. I would like the ability to annotate a function and long animation frame would use that name instead of something generic like ResizeObserverCallback. In the example below, I would like the name to be either SourceOfCode or ResizeObserverCallback(SourceOfCode) or something like this.

const func = ([entry]: ResizeObserverEntry[]) => {
    // do work
};
func.loafName = 'SourceOfCode'
const resizeObserver = new ResizeObserver(func);
resizeObserver.observe(target.current);

I do not care about the mechanism or the syntax to achieve this.

Activity

  1. noamr commented on Oct 3, 2023

    @noamr
    Collaborator

    Thanks for posting the issue!
    Note that scripts have a sourceLocation property, which includes the function name. Their syntax is function@scriptSourceURL:characterIndex.

    So the way to do what you specifically asked for is:

    new ResizeObserverEntry(function SourceOfCode([entry]: ResizeObserverEntry[]) {
      // ...
    }).obeserve(target.current);

    And this would appear in the entry's sourceLocation as SourceOfCode@myScriptURL.com:123 or some such.

  2. jarmit commented on Oct 3, 2023

    @jarmit
    Author

    This makes sense. I can make this work. For my specific case I think I would want something like

    const cb = ([entry]: ResizeObserverEntry[]) => {
      // ...
    }
    Object.defineProperty(cb, 'name', { value: 'SourceOfCode' });
    new ResizeObserver(cb).obeserve(target.current);
  3. noamr commented on Oct 18, 2023

    @noamr
    Collaborator

    Reopening, there are use cases where this can really be useful. CC @mmocny

  4. reopened this on Oct 18, 2023
  5. mmocny commented on Oct 18, 2023

    @mmocny
    Contributor

    Reading the original request, I see reference to:

    function is a common function that is used multiple times within the code base

    But later I only see reference to just making sure a nice name value is exposed. Is it actually important to somehow differentiate the "context" from which a particular callback is invoked? I'm not sure how often that would come up...


    Second, if you really want to control labelling, and do this in a way where the developer is explicitly provide a hint-- I wonder if just regular User Timings aren't both sufficient and more flexible?

    Just performance.measure() the time spent in specific sections of code (function boundary or whatever you want) and then use LoAF script attribution timestamps to intersect time ranges of your user timings.

    That way, you can measure all the time ranges for that code, but also filter to cases where it contributed to an overall long-running script within a LoAF.


    That said, I think it may be valuable if LoAF could do more attribution splitting automatically, on finer boundaries beyond just "tasks". For example, in cases where multiple continuations are registered on a shared promise. Or where a script calls back into previously registered handlers, but in ways where there isn't a 'hop' though some web platform api (like setTimeout) etc.

    One (far fetched) idea that comes to mind is perhaps any time an AsyncContext is explicitly switched, if that api ever does take off?

    Of course, a last option is to just rely on yield points, and use that as additional motivation for developers to just yield!

  6. noamr commented on Oct 19, 2023

    @noamr
    Collaborator

    Reading the original request, I see reference to:

    function is a common function that is used multiple times within the code base

    But later I only see reference to just making sure a nice name value is exposed. Is it actually important to somehow differentiate the "context" from which a particular callback is invoked? I'm not sure how often that would come up...

    Second, if you really want to control labelling, and do this in a way where the developer is explicitly provide a hint-- I wonder if just regular User Timings aren't both sufficient and more flexible?

    It is, but it can also add a lot of noise. The difference here is that this is "measure but discard if it's not a LoAF".

    That said, I think it may be valuable if LoAF could do more attribution splitting automatically, on finer boundaries beyond just "tasks". For example, in cases where multiple continuations are registered on a shared promise. Or where a script calls back into previously registered handlers, but in ways where there isn't a 'hop' though some web platform api (like setTimeout) etc.

    One (far fetched) idea that comes to mind is perhaps any time an AsyncContext is explicitly switched, if that api ever does take off?

    Of course, a last option is to just rely on yield points, and use that as additional motivation for developers to just yield!

    Those are interesting ideas, I still don't see anything actionable there but let's continue the conversation.

  7. gilbertococchi commented on Nov 7, 2023

    @gilbertococchi

    +1 to consider improving this use case.

    There are several RUM providers wrapping Callback/functions that would be flagged by LoAF as Long LoAFs triggers hiding the real Script sourceLocation information...

  8. transferred this issue fromw3c/longtaskson Mar 11, 2024
  9. patrickhousley commented on Mar 28, 2024

    @patrickhousley

    New Relic RUM needs some way of informing developers that hook into the LoAF API directly that our wrapper is not the cause of their long animation frames. I was going to look at maybe just overriding the name on the function given the wrapped function has a name property but this is probably going to cause more issues, especially around stack trace cleansing, even if it does work.

    Ultimately, we need a way to direct the developer back to the offending code and I don't think having just a name is sufficient. Even if we could update the name, if the line numbers and file name still point to the RUM provider, developers will still be confused.

  10. noamr commented on Jul 11, 2024

    @noamr
    Collaborator

    Coming back to this, I think a potential way to address this is an additional attribute in {mark|measure}Options that helps filter user timing entries, with one of the options being to include those entries in the buffer only if they contribute to a LoAF (or a LoAF long-script).

    So a library like NewRelic as mentioned in #3 (comment) could wrap the user's function with performance.measure("user-function", {filter: "long-animation-frame"}) (strawman) and then it would be a much better insight than just the entry point.

  11. mmocny commented on Jul 12, 2024

    @mmocny
    Contributor

    with one of the options being to include those entries in the buffer only if they contribute to a LoAF (or a LoAF long-script).

    It seems to me like you would have to buffer these at first, then clean up these marks and measures afterwards.

    Today, there is no limit to the number of marks/measures in the buffer, so overflow is not really a problem (unless so many are issued that we get into overall memory usage worries).

    I think that means that: just adding marks/measures, leaving them there, then filtering manually, might be sufficient?

    What about just a getEntriesBy* variant that accepts both a type and also a time range (and optionally that this time range can be specified by passing another entry)?

    The usage would be something like:

    • Create a PerformanceObserver for LoAF
    • From each LoAF entry observed, ask for the marks/measures that overlap its timing
  12. mmocny commented on Jul 12, 2024

    @mmocny
    Contributor

    As per your original proposal, Related questions:

    • It seems not so common to me to have active PerformanceObservers for marks/measures, but if someone did have one, would we want to report these LoAF-only entries?
    • If we did, would we delay adding these entries into the timeline until after we know if a LoAF happened (i.e. a separate buffer)?
    • If we did that, would we wait only until we know a LoAF is being measured, or wait for the LoAF is done measuring (i.e. LoAF always reported first)?
  13. mmocny commented on Jul 12, 2024

    @mmocny
    Contributor

    Also also, there have been requests to add groups/categories/labels to User Timings multiple times for multiple purposes. I wonder if this really needs to be LoAF specific or if it could just be a generic feature like that? Then library authors could use as they see fit and might be more creative that we are here?

    I guess the question I would have is: is it likely that a library like NewRelic would want to consume marks/measures that were created by a different app/library or just their own? Would NewRelic want to share annotations that would be useful to consume by others? (Such that a common standard us useful?)

  14. rviscomi commented on Jul 12, 2024

    @rviscomi

    cc @and-oli (maybe some overlap with DevTools extensibility)

  15. noamr commented on Jul 12, 2024

    @noamr
    Collaborator

    with one of the options being to include those entries in the buffer only if they contribute to a LoAF (or a LoAF long-script).

    It seems to me like you would have to buffer these at first, then clean up these marks and measures afterwards.

    Right

    Today, there is no limit to the number of marks/measures in the buffer, so overflow is not really a problem (unless so many are issued that we get into overall memory usage worries).

    I think that means that: just adding marks/measures, leaving them there, then filtering manually, might be sufficient?

    It's suboptimal in terms of both efficiency and ergonomics. By doing this we're encouraging people to potentially add a massive amount of events to the buffer and these memory (and lookup efficiency) problems can potentially explode. It also adds a lot of noise to the performance timeline and to traces.

    What about just a getEntriesBy* variant that accepts both a type and also a time range (and optionally that this time range can be specified by passing another entry)?

    The usage would be something like:

    • Create a PerformanceObserver for LoAF
    • From each LoAF entry observed, ask for the marks/measures that overlap its timing

    See above

    As per your original proposal, Related questions:

    • It seems not so common to me to have active PerformanceObservers for marks/measures, but if someone did have one, would we want to report these LoAF-only entries?

    I think not, perhaps attach them to the loaf.scripts entry?

    • If we did, would we delay adding these entries into the timeline until after we know if a LoAF happened (i.e. a separate buffer)?

    Yea definitely.

    • If we did that, would we wait only until we know a LoAF is being measured, or wait for the LoAF is done measuring (i.e. LoAF always reported first)?

    Perhaps in the same callback, as in if you observe both types you'd get them concurrently? Otherwise in PerformanceScriptEntry which is perhaps more suitable.

    Also also, there have been requests to add groups/categories/labels to User Timings multiple times for multiple purposes. I wonder if this really needs to be LoAF specific or if it could just be a generic feature like that? Then library authors could use as they see fit and might be more creative that we are here?

    I don't think this is related. One is arbitrary metadata and the other is a capturing condition.

    I guess the question I would have is: is it likely that a library like NewRelic would want to consume marks/measures that were created by a different app/library or just their own? Would NewRelic want to share annotations that would be useful to consume by others? (Such that a common standard us useful?)

    I feel that those are parallel question.
    NewRelic can use mark/measure names or new metadata we define to filter out what they care about, but that won't help with exploding the buffer with LoAF-attribution-tracing.

    An additional thing I would propose for this type of mark/measure is that you can't mark/measure it before/after the fact, as in the timestamp has to be now or at least within the current script entry.

  16. mmocny commented on Jul 12, 2024

    @mmocny
    Contributor

    All of that makes sense in isolation, it just feels needlessly coupled to LoAF data.

    What if I wanted to review these marks when they overlap with an Event Timing (but didn't trigger LoAF specifically because delays are from after-main-presentation)

  17. noamr commented on Jul 12, 2024

    @noamr
    Collaborator

    All of that makes sense in isolation, it just feels needlessly coupled to LoAF data.

    What if I wanted to review these marks when they overlap with an Event Timing (but didn't trigger LoAF specifically because delays are from after-main-presentation)

    I agree, I think the API should be attachable to any platform entry type where it makes sense, not coupled with LoAF specificially, or at least written in a way that's extendable to do that.

  18. mmocny commented on Jul 12, 2024

    @mmocny
    Contributor

    Can we evolve your proposal slightly to something like this, then?

    • performance.mark / measure merely supports buffered:false, or something like it, rather than explicitly listing the type of entry to pair with.
      • This has also been independently requested.
      • it seems some of the performance issues with User Timings is around the need to clone various data (like details) in order to persist on the timeline. But several use cases don't need that feature and only conditionally observe.
    • PerformanceObserver supports { observeUserTimings: true } or even { type: 'entry-type', observeNestedType: 'other-entry-type' }, or something like it.
      • This becomes like a temporary nested observer which is only observes the nested type when the main entry type is actively being observed.
      • Perhaps instead of list.getEntries() you would need to list.getNestedEntries()

    (Wonder if this would be useful to e.g. measure all Event Timings and then only nested LoAF entries, and then only nested user timings of those...)

  19. noamr commented on Jul 12, 2024

    @noamr
    Collaborator

    Can we evolve your proposal slightly to something like this, then?

    • performance.mark / measure merely supports buffered:false, or something like it, rather than explicitly listing the type of entry to pair with.

      • This has also been independently requested.
      • it seems some of the performance issues with User Timings is around the need to clone various data (like details) in order to persist on the timeline. But several use cases don't need that feature and only conditionally observe.
    • PerformanceObserver supports { observeUserTimings: true } or even { type: 'entry-type', observeNestedType: 'other-entry-type' }, or something like it.

      • This becomes like a temporary nested observer which is only observes the nested type when the main entry type is actively being observed.
      • Perhaps instead of list.getEntries() you would need to list.getNestedEntries()

    (Wonder if this would be useful to e.g. measure all Event Timings and then only nested LoAF entries, and then only nested user timings of those...)

    Riffing on that, perhaps if we add { buffered: false }, we can do the trick where if you have an observer that's registered to both LoAF (or event-timing) and user-timing, the user timing entries would be buffered to the LoAF, and the LoAF would be buffered to the event-timing entry if applicable, and the entries object in the callback would have some utility functions to correlate, and call it a day?

    // in your function
    performance.mark(functionName, { buffered: false });
    
    // the observer
    const observer = new Performance Observer(entries => {
      const [loaf] = entries.getEntriesByType("long-animation-frame");
      const [script] = loaf.scripts;
      const marks = entries.getOverlappingEntries(script, {type: "mark" });
    });
    observer.observe({type: "long-animation-frame"});
    observer.observe({type: "mark" });
  20. mmocny commented on Jul 12, 2024

    @mmocny
    Contributor

    I like that!


    Regarding { buffered: false } I think that might have been a bad suggestion in hindsight.

    1. You would still pay the performance cost of creating the entry for this use case, even if lifetime is short, and even if it's a convenient way to manage cleanup
    2. By not buffering at all, it motivates developers to load PO's eagerly, which we don't want to motivate
    3. A normal PO for just "mark" types I guess would see all the marks in real time. I know I said PO's for marks aren't as common, but that might still be undesired here.

    I think we need to go back to the idea of this mark type being detached from the typical perf timeline, yet still allow limited buffering (Perhaps more constrained than typical user timings).

  21. noamr commented on Jul 15, 2024

    @noamr
    Collaborator

    Reciting internal conversation with @mmocny: I feel that turning this into a generic user-timing function has enough limitations to make it suboptimal:

    • It's hard to do efficiently without clobbering existing receivers of user-timing or creating new GCed objects
    • It loses information about the function we want to trace, such as its source location.

    So instead, I propose to go back to the original more narrow-scoped proposal, to annotate functions for use in LoAF:

    function addEventListenerWithWrapper(event_type, internal_function) {
       const wrapped_internal_function = performance.bind(internal_function);
       addEventListener(event_type, wrapped_internal_function);
    }

    Where performance.bind will have the exact signature of Function.prototype.bind but would wrap the internal function in measuring monotonic timestamps (performance.now()), and if the result is >5ms this function would appear in LoAF as a long script.

  22. mmocny commented on Jul 15, 2024

    @mmocny
    Contributor

    Will there be a mechanism to provide a user-defined name, or will it use function.name?

  23. noamr commented on Jul 15, 2024

    @noamr
    Collaborator

    Will there be a mechanism to provide a user-defined name, or will it use function.name?

    I think we can do that but then we can't use additional arguments for bind. Perhaps that's ok

  24. noamr commented on Jul 19, 2024

    @noamr
    Collaborator

    Summarizing WG discussion:

    • There is a big ask for this, some people avoid using LoAF because of this issue.
    • The issue is split into two:
      (1) attribution: with wrapper functions it seems like "it's the wrapper's fault that this script is long"
      (2) diagnostics: a script entry point is not always granular enough.
    • In addition, people were complaining about lack of support for source-maps in LoAF and that that constraints the usefulness of source location.

    For (1), we need something reliable like function wrapping, as custom marks/measures don't give source location.
    For (2), something like a low-overhead mark/measure that ties to particular entries is perhaps preferable as not everything is a "long function".

  25. noamr commented on Jul 19, 2024

    @noamr
    Collaborator

    A shape that came to mind:

    // entryTypes can be LoAF, event, measure, script
    const tracing = new PerformanceTracing({threshold, entryTypes, detail})
    
    // This adds a mark/measure that gets applied to overlapping entries
    // from the given list
    tracing.mark(label);
    tracing.measure(label);
    
    // This creates a bound function that reports the labeled trace,
    // and also appears as a `PerformanceScriptTiming` entry in LoAF
    tracing.bind(wrapped_function, label)
    
    Performance{User|LongAnimationFrame|Script|Event}Timing.traces
        PerformanceTraceEntry (start, duration, name, detail)

    The advantage of creating a PerformanceTracing object or some such in advance is that we don't incur the overhead of creating options dictionaries every time we trace.

  26. kurtextrem commented on Mar 26, 2026

    @kurtextrem
    Member

    I like the idea. Currently I add perf.mark for mousedown/up, hover etc. just for the single purpose of knowing what is (or might be) related to a LoAF - for example expensive hover animations that started right before a click.
    Doing it in perf.marks is (relatively) expensive, but it's a great (the only?) way when combined with RUM to investigate INP thoroughly. Some providers like Debugbear collect those marks and then visualize the "timeline" of events. So a more performant alternative would be very much appreciated.

  27. guohuideng2024 commented on Mar 26, 2026

    @guohuideng2024
    Member
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions