-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Reactive graph transformations #67
Comments
Expect a more extensive answer from me later today, but a quick question: the current setup with var transformState = function(state) {
stateCalc++;
return state.name + state.todos.map(function(todo) {
return m.transform(todo, transformTodo);
}).join(",");
}
var transformTodo = function(todo) {
todoCalc++;
return todo.title.toUpperCase();
}
m.autorun(function() {
mapped = m.transform(state, transformState);
}); or var stateTransformer = createTransformer(function(state) {
stateCalc++;
return state.name + state.todos.map(function(todo) {
return todoTransformer(todo); // note: could be passed first class to map
}).join(",");
})
var todoTransformer = createTransformer(function(todo) {
todoCalc++;
return todo.title.toUpperCase();
})
m.autorun(function() {
mapped = stateTransformer(state);
}); I think I slightly prefer the second option. It makes especially clear that you should be putting inlined closures into I don't like the Maybe instead of const transformedStateController = stateTransformer.root(state);
console.log(transformedStateController.value); // always returns the up-to-date transformed graph
transformedStateController.dispose(); // abort the transformer |
I think your usecase is a nice one for graph transformations. The nice thing (theoretically, yet to be proven) is that the overhead of a single change in the data tree is (almost) constant, now matter how many items there are. This fits nicely into the @observer decorator which in itself also already exhibits this behavior. I think after introducing For very fine grained control over when updates should be propagated it is always possible to not use observable structures and change an observable timestamp or tick counter to push updates. Reactive views and values can currently be in three different states: ready, stale and pending. If mobservable allows to create values for which you can control this state yourself that could provide a useful way to manage batches as well (a view will never recompute as long as one of its used values remains stale). But I'm not sure whether this wouldn't collide too much with core values of mobservable; glitch-free, synchronous updates. But anyway, I'm very interested how the reactive transformations will work out, so I'll focus on that first ;) |
I've pushed an initial implementation to this branch: Would you mind trying it? Just clone the repo and run The api:
Note that the created transformer functions act as cache, so make sure to create them only once! Only one argument might feel like a limit, but note that you are free to access any reactive value from the source object, the function closure etc. The transformers will respond to that. Quick example: const transformState = m.createTransformer(state =>
state.name + state.todos.map(transformTodo).join(",")
)
const transformTodo = m.createTransformer(
todo => todo.title.toUpperCase(),
(todo, text) => console.log("Bye", todo.title, "aka: ", text)
);
// use the transformation
var transformedState;
m.autorun(() => {
transformedState = transformState(state);
});
// or:
var transformController = transformState.root(state);
transformedController.value; // contains the transformed graph, note that the pointer might change over time!
// or just in some class:
@observable get transformed() {
return transformState(state)
} Any feedback is welcome! Note that there is no extensive test suite yet, so if you run into any issues; don't stare too long at them and just report them :) Thanks for trying out in advance! |
I solved this problem already for Derivables. Took me a whole mind-bending day but I wrote a fairly verbose discourse on how I got to the solution. Might save you some time :) |
@mweststrate I'm just seeing this now so will look at it and provide feedback in the next few hours. Thank you! Update: ran out of time and will pick up in the morning (PST). |
@ds300 thx for the pointer. I think it is a bit simpler to solve in mobservable as I can just observe the splice events and map those. Cool thing is that this can be done to most other common operations as well, filter, slice, sort etc. Did something in the past already (in a project that was further too complicated to be viable :-P.) Biggest question for me is how you can avoid / cache when people create the map inside a closure using the closure (they might be used to do that). For example the following snippet would create a 'new' reactiveMap all the time: ** Naive approach ** class stuff {
@observable projects = [ /* projects */ ];
@observable get filteredProjects() {
return this.projects.reactiveMap(project => project.favorite);
}
} ** Create the reactiveMap only once ** class stuff {
@observable projects = [ /* projects */ ];
filteredProjects = this.projects.reactiveMap(project => project.favorite);
} ** Memoizing reactiveMap ** class stuff {
@observable projects = [ /* projects */ ];
@observable getFilteredProjects() {
return this.projects.reactiveMap(project.isFavorite);
}
} But the differences are quite subtle, so for me the primary question is: how to make this clear / educate the users? |
@kmalakoff: Conceptual question: currently the graph transformation doesn't keep parts of the graph up to date as long as there are no observers (such as autoruns or @observer components). Is this nice for effeciency or would it be more predictable / efficient if the whole graph is always kept in sync? So suppose your project tree contains 1000 transformed folders, but your UI displays only 10 of them. Would it make more sense if only those 10 transformed folders are kept in sync (making it cheaper to keep the graph in sync but more expensive if you start displaying 10 other folders) or should they be kept all in sync at all times (making it far more easier to inspect the transformed graph in non-reactive code as no transformations need to be applied lazily)? I guess the later makes more sense as it is easier to understand / more predictable what the transformation does? The current implementation takes the first approach. |
@mweststrate let me give a concrete scenario...let's say I have a TreeNode like:
I would expect that only accessed observable properties are tracked by the transformation and that this is the main method of letting the transformation know what to watch. (Note: in KnockoutJS we had peek() so that if you need to traverse the children to find a node or reduce something like Scenario 1: Add a subfolder / child to a TreeNode In this case, I would push a new node into the parent's children potentially triggering the reprocessing. If the parent node is visible, it's children should be tracked and so the transformation would be triggered on that node again. If the parent isn't visible, its children would not be tracked so the tree insertion should not re-trigger the transformation. Conclusion: I think that means the Scenario 2: Change the tags filter The root node and all nodes up to where the subtree does not contain the tag (using peek to avoid unwanted dependencies in the subtree tag reduce), but no tracking should be done on the node tags since the tree needs to be rebuilt when the search filter changes. Conclusion: The node tags shouldn't be tracked, but the dependency should be only made on the search tag...I think that means the Scenario 3: Change isCollapsed on a TreeNode This is a local dependency that is may cause a batch of subtree Nodes to be added or removed. If the transformation is to an array (not sure if array is the only target type, but it is enough for me), those new nodes should be spliced in / out somehow. Conclusion: I think that means the I think everything should be incremental and Scenario 2 is the interesting one. As for developer productivity, special tooling might handle it (like toggling an observable in the console and logging the transformation actions) rather than needing to track more for inspection purposes. I'm not sure if this helps (or actually answers your question), but this is what I would expect! |
@mweststrate on the reactiveMap question Personally, I think this is probably a non-issue since it is based on a general understanding of JavaScript. If I use a getter like If you draw attention to it in the docs, you can save people time to figure it out the hard way. Would this be a good workaround?
|
Let's call the different approaches eager and lazy. Lazy: only evaluate if there is an observer (switch to eager) or if somebody accesses the value (just evaluate lazily but don't track further) The risk of lazy is that you have a lot of unecessary evaluations if observers hop on and hop off and in the meantime the outcome of the derivation doesn't change. The risk of eager is that you have a lot of unecessary evaluations if parts of the graph are never observed. Lazy is the current behavior as it is the default behavior of mobservable. Scenario 1: Scenario 2: Mobservable gurantees that if something like that happens each transformation will be recalculated only once by ordering them correctly. So if both parent and child needs a re-evaluation the child will be re-transformed first. Scenario 3: Too look more philosophical at scenario 2, I would also depend on TreeNode tags: Good points on the reactive map question, probably people will figure out soon enought when it is clear that reactiveMap itself returns a reactive structure. Your workaround is valid indeed. |
@mweststrate I'll respond in a second. I've been struggling a bit to get the test finished (troubleshooting dependency cycles when I start using observables on the DisplayNode) but I thought I'd share my work-in-progress rather than delay further: kmalakoff@688b1f8 One change I needed to make was to not force the array items to be mobservables: kmalakoff@688b1f8#diff-510d154cf95f37822e100bfd4e7a03cdL84. This restriction was a little too strict because an object may have observables, but not be an observable. Let me know how you'd like to work on the tests with me...I've added you to my repo to keep things simple. |
I've added lifecycle checks. Another piece of feedback is that maybe the argument order should be reversed since you are likely to be more interested in the transformed node (at least that was my first intuition):
It is not really important though, more of an observation. At any rate, it seems to be working totally smoothly as one would expect! Really awesome! |
@mweststrate OK. I've written a bunch of tests and there's only one problem with a transformed node not being released. Summary
This is really awesome! Thank you! |
On the eager vs lazy discussion:
I am planning on using the transformed result to make exactly what I want to render so I need to step outside my thinking to imagine when this would come up. My gut says that unless there is a concrete, compelling use case (I haven't spent much time thinking about this, but maybe there is?), it should probably match the default, eager behavior of mobservable since it is what people probably expect, eg. sensible defaults, and when a use case presents itself through a GitHub issue, implement lazy and enable it through an option or mobservable.lazyTransformation. On Scenario 2, I skipped it in my tests so I'll need to implement it and probably some more similar tests. I won't have time for this until Thursday. I was thinking that the whole tree could change when the search tags change so every node needs to be visited anyways, eg. unlikely to benefit from incremental algorithms. |
@mweststrate I found some time and wrote the tag tests. I made a static (eg. global filter) and dynamic version (incremental), and it seems amazing how optimized this is. When I put it in my application, I'll try performance testing. That said, in order to use dynamic tags, I'll need your feedback on because just adding a dynamic array slowed things down in the creation of node: #68 I added a dev dependency on lodash.intersection for the tags test: https://github.com/kmalakoff/mobservable/blob/feature/transform/test/utils/transform.js#L3. If you don't mind small dependencies, you can leave it in, but if you try to keep everything minimal, maybe a special, hand-crafted version could be written (so I put it with the utils). Also, I wasn't sure how to implement collapsed or expanded on the display nodes themselves in a clean way so I went with an external map (although a set would have been the better choice). The problem is guaranteeing persistence while the nodes are created and destroyed with the filter so the external representation may be the best choice, but also because in the transform function, I am only passed the node, but because the transform hasn't been completed, I cannot easier find the display nodes in it. I could cache the display node on the tree node, but ideally multiple trees of display nodes could be created if you want to transform the tree in different ways. Not sure if there is a better way, eg. passing the inflight transformed results perhaps? Finally, I'd love if you can review my test code and look for better ways to do things to provide me feedback. For example, I never used the transformState pattern or transformController / root pattern so it wasn't clear if they were actually needed. Really, I just want to keep the state.renderedNodes in sync with the transformation pipeline with the least boilerplate possible (ideally PS: I looked at #2 (above):
Basically, a display node was not being destroyed, but I found that changing where I tracked the dependency made the problem go away: Bad
Good
So I don't understand understand why one is the good place to track a dependency and the other one is not! I'm not sure if there is a self-evident way to handle this or if docs are enough or if the dependency can be tracked in either. Anyways, it means all of my test cases are passing! Very awesome...I feel like I should migrate my app over! I can't wait for your blessing on when it is ready... |
Thanks for the extensive testing! Really useful! I'll try to answers the questions in order, just let me know if I missed something: Concerning (1): The looser is observableCheck is fine. Imho. Probably we should still check if Concerning (2): I don't think the link resolves anymore Concerning (3): Swapping the arguments of cleanup makes sense. Concerning (4): that is actually what the .root is for; if your root transformer will return new objects the root transformer will make sure it is stored in its .value property. Note btw that you can also expose children and icons as properties in TreeNode by doing Concerning (5): Could you file a separate request for observable sets? For eager versus lazy, I'm currently writing some benchmarking test so that a more educated decision can be made on which approach to choose. Concerning #68, what is the Adding more development dependencies is no problem. The collapsing state is interesting. At least it should be state somewhere; that is either on the state object or on the source folders. It could be state of the displaynode as well, but then the state would be lost as soon as the displayNode is no longer visible. I'm not sure whether that is acceptible in your case, so I think your current solution is fine. Concerning transforming the first item; a getter is a nice way to keep the render pipeline clean (assuming you observe that getter somewhere). I'll try to review your code more in depth later today, but I think in general this setup is fine! Concerning the transformer difference; there are actually two styles of transformers: The ones that return a reactive data structures and those that don't. For the first (your case), you don't want the transformer to run ever again because the returned DisplayNode can react directly to the source TreeNode and there is no need to create a new one (unless it was really disposed). For the second category of transformers, which don't return reactive data structures (like the first tests in the test suite) you want the transformer itself to always run again to keep the transformed data in sync with the source data. The |
This is fun...you make it look easy! (1) The looser is observableCheck is fine. Imho. Probably we should still check if value !== null && typeof value === "object". Great. I wasn't sure about it but scratched my head for a while trying to minimize the hoops I was going through syntactically. In Knockout, the API is a little more symmetical - get() and set(value) so with the various options available to me in mobservable, I sometimes find myself trying to figure things out. For example: Based on my experience with Knockout, these feel like they should be equivalent, but the second one has an error that
(2): I don't think the link resolves anymore Sorry about that. I inlined the It bad / good case in a subsequent post. You've continued the thread with (4a): that is actually what the .root is for; if your root transformer will return new objects the root transformer will make sure it is stored in its .value property. Hmmmm. The syntax / boilerplate wasn't totally clear to me for two reasons:
(4b): Is there a specific reason that you define children etc asStructure? Since transform will return (and recycle) reactive DisplayNodes it is more efficient to not use structural comparison. Like in #55, it seems like mobservable's default behavior should to leave it to me to choose if I want recursive conversion to observables...opt-in (default is shallow) instead of opt out (asStructure). I sort of think opt-in is better because I use the class notation and hand-craft which properties to observe and so I want to block the recursion meaning I've been using asStructure. It could be a problem with my understanding of mobservable, but as soon as I saw that mobservable was recursively creating observables while I was also hand-selecting them (using classes and property decorators), I read the documentation to try to figured out how to turn off recursion to give me fine-grained control...hey mobservable, hands-off my hand-optimized class instances...I'll let you know when I want you to help! 😉 (maybe I'm missing something, but there have been zero cases so far where I wanted recursive observables) It's like in Knockback, I allow all attributes and nested relationships to be made observable by default which is great for quick prototyping:
but in production code, I recommend hand optimizing each ViewModel to observe only what is needed because observables are expensive and because recursion can create deeply nested trees (even though I resolve the cycles to the same ViewModels to allow reuse and to avoid infinite recursion):
(4c): Note btw that you can also expose children and icons as properties in TreeNode by doing I did use that syntax once, but prefer a symmetric syntax like:
This could be related to my experience with Knockout. I wrote Knockback where I got in the habit of adding additional basic and computed observables in the longhand for readability even though I could have been syntactically more sugary using extend because they could be documented and grouped more easily for self-documented code. Decorators and instance variables make this type of self-documenting approach even better (instead of ES5)! Also, this could be because of my problems understanding the differences in (1) - I'm lean towards symmetry and ease of readability. (5): Could you file a separate request for observable sets? Done: #69 MW: Concerning #68, what is the findOrCreate in the profiler output?
The constructor:
I construct the tags like this for two reasons: 1) the caller doesn't need to pass in observables, eg. slightly shorter syntax, but more importantly 2) to tell mobservable that this is MW: Concerning transforming the first item; a getter is a nice way to keep the render pipeline clean (assuming you observe that getter somewhere). KM: Can you show me what you would do in a modern ES syntax considering (4a) above? MW: The return new DisplayNode statement does not observe anything; it just creates some new reactive functions but that in itself doesn't establish a reactive relationship. However, the node.icon() will cause the transformer to actively observe the node, so after that both the transformer and the node returned by the transformer will be observing the source node. I think that explains roughly the difference, although I didn't dive into the details of the second transformer yet...Probably I'll take a deeper dive into this later today. KM: Great. I'm looking forward to what you find out...If what I did when it wasn't working properly is problematic (eg. shouldn't set up observable relationships with the transformer), maybe in a debug mode you could wrap the transformer in an untracked statement and see if any subscriptions were created and warn the user? MW: what are memo and iter? KM: iter is the transformation function and memo is to collect the results recursively. Think of memo just like in reduce for an array to collect results instead of to reduce them (memo is what underscore calls it in reduce), but for recursive tree mapping. It avoids temporary and concatenating arrays.
Results or memo needs to be passed through the tree to optimally collect results, but you want to call it like Instead of creating and concatenating arrays at each node:
I think this is really turning out great! I really appreciate you devoting so much time to making this awesome... |
Regarding: var store = observable({root: null});
state.renderedNodes = state.root ? state.root.map(transformNode) : [];)
var store = {root: observable(null)};
state.renderedNodes = state.root ? state.root.map(transformNode) : [];) The difference between the first and the second is that the
In ES6 / typescript that would be: class Store {
@observable get renderedNodes() {
return state.root ? state.root.map(transformNode) : []
}
} Ah I think you got a misunderstanding about what It is perfectly fine indeed to not use ... answer to be continued, have to dine ;) |
Makes sense. One quick note (I'm on my out for the day)... MW: Ah I think you got a misunderstanding about what asStructure does, what you need is asReference. KM: I do find them confusing! I totally prefer to get rid of asX and use decorator options, but since if shallow is not the default in 2.0 and because I use decorated classes and because I don't like to have to think too much about basic decisions, I probably will ever only use one (rather than having to care if I am passing a value, struct, class instance)...like observable(raw([])) so maybe having mobservable inspect to figure out what it is. I'm not sure about the other asX variants since I haven't had a use case yet...maybe they should be creation helpers instead of asX? eg. m.structArray(values) or basic types? Eg. m.nestedObservable, childObservables, etc. Agreed that asX could be better! |
Good points. I will definitely revisit. For now just go for asFlat for On Wed, Dec 9, 2015 at 5:39 PM, Kevin Malakoff notifications@github.com
|
Btw would you mind if I merge your branch already back? The tests are very useful! |
@kmalakoff Btw knockback looks fancy! Didn't know it. I did take a lot of inspiration from knockout btw, it actually inspired me to build mobservable :) |
I'm back online... I've created a pull request for the merge. (1) Regarding: In knockout, you would do something like:
So you probably wouldn't use the observable approach for a store since you want to modify the whole store at once, but each element individually. Approaching mobservable with this frame of reference, my intuition is that I shouldn't be using the observable pattern, but the view model pattern and would expect this syntax to work, eg. mobservable uses set / get magic on root instead of root() / root(value). I sort of expect that the following is equivalent:
I think my answer is to not use observable({root: null}) nor {root: observable(null)} syntax, but only stick with the class syntax since it intuitively makes sense to me because I think of mobservable's observables like knockout observables but with get / set magic. I think it just an incorrect mental model on my side biased by Knockout...in my head, if It could also be that observable conceptually does too much (I think of it as an atom, not a molecule). Maybe there needs to be separate APIs in mobservable:
I think it was the name of the transform
Would something like this work (mind you, the class syntax is more familiar to me):
On the As for my optimization problem (#68), let me know if you agree that initial setup vs updating an observable having different code paths makes sense. Poor construction performance would be the only blocker for me upgrading to transform right now! |
I should probably say that I realize some these ideas may be extreme, based on a different mental model than you have in mind, based on poor understanding of all the use cases or ES6 language features, etc so definitely, don't worry about saying you are not going to take your library in any of these directions. I just wanted to share my impressions (and perhaps biases and misunderstandings!) since I have your ears and these were the sorts of things that came to mind when trying to make a leap from knockout to mobservable so others may have the same thoughts trying to translate concepts. I think this library is totally awesome! |
No problem! Saddly I'm a occupied today with other stuff that popped up and On Thu, Dec 10, 2015 at 8:11 PM, Kevin Malakoff notifications@github.com
|
I've been able to update my application (with the array optimizations) and it is working great! This came right at the perfect time. One thing I noticed on the API, I wanted to pass arguments to m.createTransformer (m.createTransformer((node, filteredChildren) => {}), but got the I understand why you would add this warning so only one thing is transformed. To work around this, I just put the arguments on the node itself (it was a filtered version of the children needed for tree manipulation availability), but maybe the arguments should be passed through since in my first time using it, I ran into the need for arguments? |
@mweststrate I've been iterating on more filtering and sorting cases, and I'm having a little bit of trouble doing what I need to. It is sort of related to the above (where I wanted to pass arguments to the transformer)... The problem comes from the fact that sometimes the parent's visibility is determined by it's children and that the children are dynamic based on the filters / search criteria. So first I'm creating the children recursively and then finally the parent. Here's the pseudocode:
The first time around, it works, but then if I change a tag or search criteria, the children or node itself may no longer be visible. If I could patch the children in subsequent transformation runs, I could work around it:
but there is a cycle. So I tried another way which is to make the children of the transformed node a computed / view, but then there is a cycle on the array of transformed nodes since the children cannot looked up until after a full pass through generating all of the transformed nodes (the nodes are created in the middle of the transformation cycle). I'm going to keep trying to find a way to break the cycles, but if you have any suggestions, please let me know! Update I ended up finding a work around, but because it isn't as simple of a use case as the tests given the dependencies up and down the tree to determine visibility / hierarchy, I wasn't able to rely on transformations to build the tree, but on the nodes. I ended up using transformations to just maintain a list of displayNodes and used the sort of hacks that I've been trying to avoid (the immediates, untrackeds, a variable on the store telling me that it is mid-processing, etc):
Maybe there is a more elegant way to do this? I feel like you mentioned earlier about doing things fully reactive is probably the solution, but I can't see the way to do it like that at the moment. |
Hi @kmalakoff, I won't have much time until tomorrow evening, so here a short answer. Didn't look into your example in detail yet, but from the description it seems that you try to achieve exactly the same as I did in this test: https://github.com/mweststrate/mobservable/blob/feature/transform/test/perf/transform-perf.js#L38, is that right? Would the approach from that setup work? Good point about the additional arguments. Maybe those additonal arguments should be stored then in the memoization cached and passed into the function on subsequent calls (until the same transform is called with other arguments)? I think that would work fine, I just have to make sure it isn't confusing in some way. |
@mweststrate thank you for pointing me to this. I woke up with the same thought...the children should be transformed in the display node themselves rather than in the store and pass in. I'm going to try refactoring again! Can't wait until I nail transformation since it seems like such good way to do things. |
@mweststrate these transformations are working great for reducing my reliance on the "autorun and setting observable property values" anti-pattern, and managing life cycles (I'm using observable getters and transformations to ensure destroy is called on dynamically created class instances). It is very satisfying to remove all of those hacks and to experiment with lifecycle-aware computed properties! I'm still trying to wrap my head around the transformation API though, but will report back when I've narrowed it down, but I'm finding destroy is being called when I'm not expecting it. Is there a constraint where one node can only have one transformation on it at a time? I've started running multiple transformations on the same nodes which could explain the behavior I'm seeing, but not totally sure. Need to investigate further. |
I haven't yet looked into the problem above. I spent most of the day tracking down other bugs, but will try to write tests for the above. I still have a bug which might be related to the One thing that I did need to do was wrap a transform in an untracked because it was interfering somehow with the dependency tracking (without wrapping the transformer in untracked, other dependencies in the autorun no longer triggered / seemed registered):
I'm not sure if anything comes to mind... By the way, it is working "well enough" for me for now (with some work arounds like these) to move back to feature development! I'll try to interleave writing tests and reporting bugs over the coming week so no urgency to respond to me. I really appreciate the time and effort you have put into all of this. A huge thank you! If you want to discuss any of my usability feedback or bounce any ideas off of me, feel free use the email address on my Github profile or to discuss on a Github issue. |
@kmalakoff "Don't use autorun to transform state into state" or "Automatically updating application state is an anti-pattern. Derive data instead" should be really mantra's. Its often how we programmers tend to think :) I think I should blog a bit about that. Funny enough I exactly run in to the same issue a few days ago, autorun should always be untracked. Thinking about that, I figured that autorun should always run after all other derivations have completed and in complete isolation of any current computations. So I'm refactoring / rewritting / optimizing a bit while I'm adding that (scope creep, definitely). So I'm really interested to see whether, when I've finished that, it makes a difference for your use case. Any way, thanks a lot for your enthusiasm, patience and good suggestions! |
Just rewrote history state tracking in the mobservable-reactive2015-demo repo using |
That's awesome to hear! I wonder if a comparison of before and after (eg. either just a migration or a migration plus some performance numbers) plus an explanation of the reasons why would be a good addition to the upcoming docs or for a blob post with the release of 1.2.0? |
I'm thinking about writing a blog post about For the reactive demo the minimum frame rate quadrupled and the cost of serializing the state went from 9% to 2% when drawing 10.000 boxes and arrows. Anyway, the feature has been published as 1.2.0 and is documented here: http://mweststrate.github.io/mobservable/refguide/create-transformer.html |
I'm really excited to see what you have planned for the "Reactive graph transformations" (well, I took at look at the code/tests, too!).
I'll explain my use case a little more for food for thought...
As chokidar is scanning for folders and files, it is emitting a stream of changes at a high rate so rather than processing them in an incremental fashion (which could be inefficient to render), I'm currently batching them up and them periodically merging them into the scene graph (the "source" representation) and then triggering a full scene render (the "target" representation) which then gets passed down through React to render "reasonably" statically. The way I am currently triggering the render is 1) by using a "modifiedAt" observable timestamp at the top level since there is a single root node and I'm using transactions both in the scene graph and rendered representation phases 2) whenever the view settings change (like the search filter).
So I think there are three main needs:
ease and efficiency of an incremental representation - because I'm batching change streams at the moment, I have made child relationships simple arrays instead of observable arrays because I could have peers of 500+ folders that would keep triggering changes during the batching process. If this was made incremental, I would need to search for where to insert the child into the scene graph and then incrementally determine if the generated child needs to be added to the rendered representation and where to put it.
an efficient way to regenerate the full rendered representation from scratch when the view settings change significantly
synchronicity and render management - because there is an asynchronous flow of chokidar events which may themselves cause more async file system calls, there still might be a small batching step where changes are being resolve asynchronously and them merged into the tree. If many changes appear at once, there may need to be a certain amount of rate limiting based on a framerate target. Like in computer graphics rendering, you sometimes drop frames instead of trying to keep up with an increasing backlog of work. Right now, I'm just measuring each phase and using it to set the maximum number of nodes to process and throttling updates accordingly.
If any of this is way too complicated for the short term, too edge-case, or there are good shortcuts, let me know. It's easy for my to imagine best case scenarios! I'd be happy to discuss and beta test...
The text was updated successfully, but these errors were encountered: