-
Notifications
You must be signed in to change notification settings - Fork 0
Migrate to push-based data flow for tracer #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
@tegefaulkes @CMCDragonkai need some inputs for this |
I originally wanted to integrate rxjs observables into Polykey library. But otherwise libraries usually expose event emission that can be turned into an observable flow. So you should be able to do both. Keep the libraries doing event emission as normal, then use the wrapper functions provided by rxjs and ixjs to then turn it into observables. Idea is observable subjects becomes part of the Polykey core. |
Okay, so I need to basically create an Event emitter here (not streams, that could be a wrapper if we need it) and use an unbound queue to let the events grow as needed. If we allow multiple callbacks, then this introduces another interesting point. If we queue stuff in the memory for multiple callbacks and one callback is slower than others, that would hold up other callbacks before that element can be removed from the queue. On the other hand, we can create queues per callback or event handler, but that would result in a massive increase in the required memory. Also, we probably don't need to use |
No we are using js-events as the foundation. |
This requires empirical observation. No need to theory craft. |
We'll need to use Benny to do benchmarks of the different methods. The main problem here is that tracing could end up blocking the main process in a crippling way. So the way we inplement matters. We need to explore the usage of webstreams, observables and eventTarget to see how these methods of data-flow perform. We also need to explore how much buffering will help here. Compare each method to
Really we could implement a stream and observable method at the same time. they each have their pros and cons. I'd prefer a webstream for piping into a file for the back-pressure support. While an observable would be more useful for debugging in real-time. |
I went over this with Brian, and this is what we came up with. Firstly, we need to create an observables foundation. Brian walked me through the basics of rxjs observables for this. As we know, observables are completely synchronous, which means they block the main loop execution to ensure order. This goes against tracer being transparent, as the very nature of observables will cause the main process to take a hit. The approach we finalised is basically using observables as a potential egress point, but also registering a webstream to handle the events. As webstreams internally buffer the events, we get a free unbound buffer which adapts to the pace at which the consumer is consuming data. In conclusion, first I will integrate observables into the codebase, then add a webstream wrapper around it to get a quick-and-easy buffer over the incoming data. I will write benchmarks to back the claims with empirical data. |
Observables are synchronous because "push" is synchronous. It's basically a function 1 calling function 2 calling function 3. This is fine - because the amount of work taking place isn't that much. By async sink, I'm talking about the final sink point, if that ends up calling an async IO operation, that then goes into the event loop and it is async. |
I'm not sure if webstreams make sense here? The idea behind observables are purely object oriented. You only use web streams when it becomes IO. I would not make these sorts of things a web stream unless it's doing IPC. An async file sink is not exactly IPC and therefore doesn't need to be a webstream. |
Eventually, we will need to use spans as decorators. We can handle doing that kind of like how When we create a class and we want it to be traced, it needs its parent context to know where we are being traced inside — what is our parent. This is generally not possible as the callee will not know who the caller was generally. However, it becomes possible in meta programmable languages. Javascript supported it before, where callee could access the caller object via the following approach. This, however, is now deprecated and not supported under strict mode.
But we have already solved this problem in
Note that after talking with Roger about this, the conclusion we arrived at is to not worry about where the span ended, only that it actually ended — leaks will be found by observing open spans, not which context they ended up in before being closed. |
There should be 2 visualisations - one a vertical lines and forking system - and another a dynamic graph that shows the concurrent trace lines. Ideally they should be correlated visualisations over the time domain. |
Class and Method decorators already provide a syntax structure way of identifying your parent span. However inline creations of objects as well as creating the objects themselves from the classes don't have a clear syntax structure. As in syntax nesting does not map one to one to "dynamic" lifecycle context. Lifecycle context - is more akin to "memory management" and we know that generally speaking, it's somewhat related to Rust's ownership model. Thus means it's more dynamic in a language runtime of JavaScript. So our visualisations are just a starting point for a comprehensive diagnostic tool - not to specifically solve say the current memory leaks we are encountering, but to solve potentially unbounded problems we haven't met yet. It will become a critical tool used by all developers. |
Potential stub objects/classes:
"Closing the Loop" means that everything that starts eventually ends. Everybody eats their own shit. Buddhist Programming lol. |
I kind of dislike the usage of See this is actually possible with: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef Now why a weakref? Well the main idea here would be that it's only relevant as long as the "context" is still active. If the span context no longer exists... then it's not relevant anymore right...? I guess the question is this fundamental ambiguity here, when we are doing tracing, are we tracing one of these 2 things?
See tracing 1. is easy enough. Tracing 2. requires dynamic updates, because ownership can be moved. Consider a promise that gets created in one context, but ends up being awaited for in a separate context. In this case, the lifecycle beings in one span but ends in a different span. The visualization of this would require a sort of "portaling" of the lifespan line (I can imgaine a zigzag notation indicating teleportation/movement of ownership). This concept might make more sense with some more conceptual compression with AI. |
Yes, I believe Let's summarize why this trio works and can be considered "minimal" for the stated goals: 1. 2. 3. Why this set is "Universal" and Addresses Your Core Requirements:
Could we make it more minimal?
Therefore, this trio seems to hit a sweet spot:
The real "work" is then in:
This feels like a solid foundation. |
Okay, here's a draft of a comprehensive comment you could adapt for the Subject: Proposal: Evolving Hi all, Following up on the discussion around enhancing The current Core Goal: To evolve
This will enable advanced diagnostics, including robust leak detection (for promises and other resources), performance analysis that considers resource contention, and clear visualization of complex system interactions. Proposed Conceptual Framework: The Minimal Trio We believe a minimal yet powerful set of abstractions can achieve this:
Underlying Data Model: Crucially, these abstractions are primarily for conceptualization and for the consumer/visualizer of the trace data. The runtime SDK's main output would be a stream of immutable
Leveraging
Potential Implementation Scenarios:
Key Capabilities Unlocked by this Framework:
This approach aims for a minimal set of core abstractions that can be combined to describe very complex system behaviors, all while being grounded in a streamable We believe this provides a solid conceptual foundation for the next generation of |
Okay, here's a compressed version of the visualization views, keeping React Three Fiber (R3F) in mind for the more graphical elements: Core Visualisation Strategy for A live, tiled window system where selecting an element in one view highlights/filters related elements in others. All views update in real-time from the Key Views:
Inter-View Correlation: Is paramount. Clicking an element in any view (e.g., a
This multi-view, correlated approach, potentially enhanced with R3F for graphical elements like the Interaction Graph, will provide a powerful and intuitive way to explore complex trace data. Our type system (enums, etc.) will naturally drive the filtering, styling, and categorization within these views. |
So basically I fed all this information into gemini, and it gave me back an understanding that there's 3 conceptual primitives needed.
Then the idea being that spans only really relate to function calls - synchronously. But TraceLifecycle relates to everything that can exist in the heap including both our domain objects, short lived objects, and promises. For the explanation of the the diff between Span and Lifecycle when you're dealing with say an async function call that you're awaiting within a parent async function...
Here's how it breaks down and why both are needed: Scenario:
Why this distinction is important:
Addressing the "Promises that are basically asynchronously awaited for within the same function call": This is exactly the
Where does
In summary: It's not an "either/or." For an
This dual tracking, linked by |
Then in order to understand relationships between different objects: You're asking for the core semantics of Here's a breakdown of each 1.
2.
3.
Why is this flexibility important? Real-world systems have complex interactions. Limiting links to only one type of connection (e.g., only Span-Span) would miss critical information needed to understand the full picture. |
Also I asked if what we're doing here is comparable to any existing tools out there: Predicting the exact state of tooling by May 2025 is speculative, as the observability space evolves rapidly. However, based on the landscape as of early-mid 2024 and projecting reasonable advancements, what you are proposing is still quite innovative, particularly in its holistic integration and specific focus areas. Here's a breakdown of why and what aspects are most novel: What exists and is common (or becoming common):
What makes your proposal innovative (especially the converged system):
Comparison to Existing Paradigms:
Conclusion for May 2025: By May 2025, we can expect general observability tools to be even better. However, the specific combination of:
...is likely to still be at the innovative edge or beyond what most generally available tools offer. You're essentially bridging the gap between operational tracing, low-level resource/object lifecycle tracking, and aspects of memory/reference profiling into a single, cohesive system. This is a sophisticated vision. If realized robustly, it would indeed be a significant step forward. This is also why R3F should be used as a base for our viz, as we are going to be scaling that viz and correlated viz quite importantly, so being able to do complex animations is necessary @shafiqihtsham. D3 is only useful at the beginning for prototyping. Just like @Abby010 did initially in the terminal. That was a live fire test as a forcing function to see if the concepts were understood. |
OpenRouter Chat Sun May 18 2025.json Also you can download my convo here, and upload to openrouter for further exploration and specific drilldowns. |
Uh oh!
There was an error while loading. Please reload this page.
Specification
Currently, the tracer is pull-based. The events are generated and added to a global queue, which a generator can consume. This is bad, as if no consumers are present, then the events can start building up indefinitely.
To fix this, we need to adopt a push-based dataflow, where there is a source of events which are all consumed in real-time by subscribers via callbacks. This approach draws inspiration from how
rxjs
handles this.For this implementation, however,
rxjs
is unnecessary complexity and we can make do with onlyWebStreams
orEventEmitters
internally. I'm leaning towards using streams, as streams handle backpressure for us if the consuming callbacks are slower than incoming data. However, that might mean an observable slowdown of the program if the backpressure buffer is full. To avoid this, we may need to create an unbound buffer, but that would again result in the same issue where the buffer can grow indefinitely. It would be a bit more manageable here, as events can be ignored if no consumers have been registered. This would need some discussion.Additional context
Tasks
The text was updated successfully, but these errors were encountered: