-
Notifications
You must be signed in to change notification settings - Fork 59
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
Don't allocate response objects to avoid triggering GC #153
Comments
The original API design as proposed by @rwaldron didn't have GC collection issues. The new design does, mainly because I wanted the same API to be available from the event object and the sensor instance itself. Now that we've stopped exposing the sensor reading from the event object, maybe we should reconsider exposing it as a separate object on the sensor itself to avoid GC issues. What if we tried something like that instead (here for gyroscope)?: [SecureContext]
interface Sensor : EventTarget {
readonly attribute SensorState state;
readonly attribute DOMHighResTimeStamp timestamp;
void start();
void stop();
attribute EventHandler onchange;
attribute EventHandler onactivate;
attribute EventHandler onerror;
};
dictionary SensorOptions {
double? frequency;
};
[Constructor(SensorOptions options)]
interface Gyroscope : Sensor {
serializer = { x, y, z, timestamp }; // so we can still get a reading object when needed
readonly attribute unrestricted double? x;
readonly attribute unrestricted double? y;
readonly attribute unrestricted double? z;
}; That would allow uses like this: let gyro = new Gyroscope({ frequency: 240 });
gyro.start();
gyro.onchange = _ => {
console.log("Rotation rate around the X-axis " + gyro.x);
console.log("Rotation rate around the Y-axis " + gyro.y);
console.log("Rotation rate around the Z-axis " + gyro.z);
};
gyro.onerror = event => console.log(event.error.name, event.error.message) Thoughts? |
@tobie, this design would allow getting state of data, without allocation on changes. And would allow to subscribe to data changes. This is actually is a good option here. Although, this pattern will only work for singleton sensor types - there is only one gyro, and one accelerometer ever? One slight concern, with sensors that have frequency that affects the "accumulated" data. For example accelerometer would not just get current state, but accumulated between pulls data? If that is the case, then with sync use cases, pulling could be sync too. |
Yes, via identifying parameters which are sensor type specific, e.g. (completely made-up identifying parameters): let gyro = new Gyroscope({
frequency: 240,
external: true, // identifying parameter
hand: "right" // identifying parameter
});
I've punted on dealing with accumulate date for now, tbh. I wan't to make sure we don't design something that makes this impossible in the future, however, so suggestions are welcome. |
That makes good sense. Providing default behaviour assuming it subscribes to "first", and allowing devs to enumerate available sensors and choose specific.
Indeed, it can be a tricky one. So for example with Accelerometer, current values of acceleration - is what will be available as state, but not the deltas. Developer has ability to calculate delta if he needs so in event handlers or in fixed update method (sync path). |
Specific sensor specs get to define what default should be (or if a default makes sense at all). |
Just to clarify, |
Indeed, this is just accessor to latest available data. |
@tobie Initially we've implemented API in most efficient way. The Sensor.reading was a 'current' reading, thus, no need to create new reading on every update. After discussion under (w3c/ambient-light#15), you explained how you want API to behave and that SensorReading has to be recreated for every update. So, I would propose to keep API as it is. If we want more FPS and less GC, we have to modify Sensor.reading description that will explain that Sensor.reading always points to current reading of the sensor. 'Sensor has reading' => interface Sensor { SensorReading reading; } (good design)
I know that Mikhail and I explained how we've implemented sensor reading update in Chromium, however, imo, we should avoid talking about UA specific implementation details. |
Why not just keep the existing semantics ( |
Thanks for your input @alexshalamov. Getting back to our requirements, I think we want to try and fulfill the following, somewhat contradictory use-cases:
Requirement 1) is extremely perf sensitive, and if our ability to deliver on it is hindered because we're triggering too much GC, we should definitely fix that. Requirements 2) and 3) on the other hand require that we have distinct objects representing each readings (additionally, timestamps for 3) need to have an epoch-based component and not be DOMHighResolution only). The design I'm suggesting here (which, btw, is just a suggestion), delivers on 1), but require an extra bit of work for 2) and 3); basically a way to snapshot the state of the sensor at tn. I added a serializer in the above example as a placeholder. But that could also be a method, e.g.: [SecureContext]
interface Sensor : EventTarget {
readonly attribute SensorState state;
void start();
void stop();
SensorReading snapshot();
attribute EventHandler onchange;
attribute EventHandler onactivate;
attribute EventHandler onerror;
};
interface SensorReading {
readonly attribute DOMHighResTimeStamp timeStamp;
readonly attribute DOMHighResTimeStamp timeOrigin;
};
dictionary SensorOptions {
double? frequency;
};
// E.g. for Gyroscope
interface GyroscopeValues {
readonly attribute unrestricted double? x;
readonly attribute unrestricted double? y;
readonly attribute unrestricted double? z;
};
[Constructor(SensorOptions options)]
interface Gyroscope : Sensor {};
Gyroscope implements GyroscopeValues;
interface GyroscopeReading : SensorReading {};
GyroscopeReading implements GyroscopeValues; We could even imagine building on top of this a buffer for all snapshots in between to event turns or some such. |
Because it doesn't help us with requirements 2) and 3) above, makes for more verbose code ( |
@rwaldron would love your thoughts on the above. Thanks! |
As for 2) and 3)
cannot |
Well for both cases 2) and 3) it seems developers would essentially play around with reading objects, so they woudl be doing:
Meh. I'm sure that won't be an issue for JS devs at all. There's a long tradition of favoring terseness over semantic purity in JS. They'll feel right at home with this.
Sure, but that still wouldn't resolve immutability issues. It's no longer a sensor reading if it changes over time. It's a proxy for the sensor values. |
If sensor has var lastReading = null;
accelerometer.onchange = function() {
if (lastReading) {
var deltaX = this.reading.x - lastReading.x;
}
lastReading = this.reading;
}; Someone would be confused why this wouldn't work on assumption that reading was immutable objects. But if there is no object involved, such as var lastX = null;
accelerometer.onchange = function() {
if (lastX !== null) {
var deltaX = this.x - lastX;
}
lastX = this.x;
}; There is much less chance to get confused in this case. var lastX = null;
var update = function() {
requestAnimationFrame(update);
if (lastX !== null) {
var deltaX = accelerometer.x - lastX;
}
lastX = accelerometer.x;
};
requestAnimationFrame(update); |
Yep, that's how J5 is designed as well. I've never been a fan of the The design that @tobie suggested above, which had this example: let gyro = new Gyroscope({ frequency: 240 });
gyro.start();
gyro.onchange = _ => {
console.log("Rotation rate around the X-axis " + gyro.x);
console.log("Rotation rate around the Y-axis " + gyro.y);
console.log("Rotation rate around the Z-axis " + gyro.z);
};
gyro.onerror = event => console.log(event.error.name, event.error.message); Satisfies criteria 1, 2 and 3. Here's how:
We already know why, it's because there is no excessive allocation and therefore no excessive GC demands.
Application code can easily copy the values from the properties and store them for later, if they want to do so: let cache = [];
gyro.onchange = _ => {
cache.push({ x, y, z } = gyro);
}; ... Any amount of historic data can be kept or discarded as needed by the application (re: criteria 2) |
@rwaldron, seems you're suggesting ES6/7 provides enough syntactic sugar to make a |
Definitely |
@Maksims do you have any test set (data) which shows that current approach makes 60fps target unreachable? I quckly measured codepath for sensor reading update using current API and with modified codepath that matches proposal in this issue. The difference between two is neglectable: fluctuations on a level of 0.1-1 microseconds per reading update. Tested on Mi Pad2 device.
Before changing the API, we need some data which proves that performance issues do exist, otherwise, it looks like a premature optimisation. |
Garbage collection pauses causing missed frames are well known and well documented issues. Note the issue isn't the extra allocation time, it's the GC pauses. Among the TAG requests (coming from @slightlyoff) was a requirement to make this available off of the main thread for fear of jank. Designing CPU-hungry APIs designed for perf-sensitive applications with performance in mind isn't premature optimization, it's just Doing the Right Thing[TM]. ;) That said, I'd love to see test data if there's some easily available, but I don't think the onus is on the OP to prove his point here, rather it's on us to explain how the current design is going to avoid causing GC pauses when GC kicks in. |
@alexshalamov @tobie - have answered pretty well. Every little thing matters. And if API does not provides a way to avoid allocations then developers hitting the GC issues will struggle to deal with a situation. As @tobie said:
|
@tobie There are few concerns with proposed modifications.
Chrome canary has implementation of ambient light, accelerometer, gyroscope and magnetometer. It would be good to have measurements that prove that the current approach would not allow us to reach continuous 60fps target. If required, I can quickly prototype infinite scroller with recyclable elements, so that we will see whether current approach affecting scrolling performance / fps or not. If you still think that GC would be a problem, we could consider an alternative solution. Could we instead provide a method that will fill sensor reading data, e.g. |
This fixes a number of pending issues listed below, for which there was consensus, but editing was still pending. This introduces a dependency on the infra standard [[INFRA]], to define precisely the low-level data structures used by slots and concepts. This also specifies slots more clearly. Additionally, this rewrite sprinkles inline issues in areas of the specs which require more work. * Handle GC issues by getting rid of the SensorReading interface. Closes w3c#153. Closes w3c#101. * Check sensor existence before requesting permission. Closes w3c#145. * Remove side effects from constructors. Closes w3c#138. * Move default sensor check to start() and add "unconnected" state. Closes w3c#128 and closes w3c#104. * Throttle update frequency to 60Hz by relying on requestAnimationFrame. Closes w3c#100.
This fixes a number of pending issues listed below, for which there was consensus, but editing was still pending. This introduces a dependency on the infra standard [[INFRA]], to define precisely the low-level data structures used by slots and concepts. This also specifies slots more clearly. Additionally, this rewrite sprinkles inline issues in areas of the specs which require more work. * Handle GC issues by getting rid of the SensorReading interface. Closes #153. Closes #101. * Check sensor existence before requesting permission. Closes #145. * Remove side effects from constructors. Closes #138. * Move default sensor check to start() and add "unconnected" state. Closes #128 and closes #104. * Throttle update frequency to 60Hz by relying on requestAnimationFrame. Closes #100.
Looks like constructing of small objects (like sensor readings) once per animation frame does not actually cause GC issues: https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/NJFiEsqW-RM |
The attribute will be more convenient to use. The main reason for the previous using of BYOB was the potential GC issues, now BYOB seems unnecessary for this case: w3c/sensors#153 (comment)
The attribute will be more convenient to use. The main reason for the previous using of BYOB was the potential GC issues, now BYOB here seems to be unnecessary: w3c/sensors#153 (comment)
Thanks for looking into this. After a cursory glance at the comments on the Chromium thread, it doesn't seem to address the case where you have more than one reading per frame, what happens when these objects are kept around longer than a frame (Hannes' comment about GC easily taking "several milliseconds" in certain cases is particularly worrying), or whether that has an impact in a larger application settings (which is what the OP was concerned about). It would also be important to get feedback from other implementors on this topic (which might have different GC strategies). |
This would not trigger GC stalls, since sensors of the same type will share reading, thus max number of updated readings per frame would be rather small, and each allocation is also insignificant as I mentioned earlier (less than microsecond)
These objects cannot be kept for a long time, either they are GC'd after each frame, or when sensor is stopped, then internal reading is released and the one that is reffed by the user, will be GC'd when reference is lost. It would be worrying if we would keep huge internal cache of readings for a long time and then invalidate them all at once, which is not the case for our implementation.
Very good point, but taking in to account performance of JSCore or SpiderMonkey, I doubt this will be an issue. |
A
This assumes that no application-level code interferes with this. Consider the following (inspired by a use case found in a related issue): var buffer =[];
var sensor = new Accelerometer({ frequency: 120 });
sensor.onchange = _ => {
buffer.push(sensor.reading);
if (buffer.length >= 500) {
fetch("/log-data", {
method: "POST",
body: JSON.stringify(buffer)
});
buffer.length = 0;
}
}; This also doesn't address the concern of GC issues in real life scenarios, which is what the OP was worried about.
I'd like to hear those implementors speak for themselves. |
Serious question: microsecond or millisecond? |
The thread on the chromium list talks about milliseconds. It's milliseconds I intended to write here. But maybe that thread is incorrect? Or @alexshalamov above is? |
I never said milliseconds in blink-dev, neither in this thread.
This is not a valid example, in the same way developers can populate buffer using Based on profiling data for different code paths and initial feedback from developers who are implementing GC in V8 / blink, there is no technical proof that exposing objects instead of primitives would cause "stop-the-world" JS thread stalls because of GC. Anyways, the change for the interfaces was already done, unfortunately, without any data or even JS example that represents the problem. I think we should avoid that in the future and do proper investigation before rushing to change everything. |
I never said that GC is less than microsecond either, I said that reading update, e.g., re-creating reading object (3 doubles, 12 bytes) will add extra 0.1-1 microsecond overhead. (Edit) - actually, it can be even faster if for the test, we would have used pre-allocated buffer and then placement new. |
I was quoting Hannes Payer who said:
So it would still be useful to get some clarity here. Which is it? |
As explained when that changed was done, the That said, if we have a new need for this, it makes complete sense re-exploring, but we'll need serious testing to be absolutely sure this doesn't create GC issues on any platform. Fwiw, I'm absolutely not convinced by your test case given the kind of cases where I've seen such issues crop-up in the past.
You're right from a theoretical perspective. But in practice, a JS dev not attuned to the inner workings of Chrome GC, will probably assume That said, that begs the question as to whether we really want to expose sensor.x, sensor.y, and sensor.z on high-frequency sensors (I have no idea—I'm just thinking out loud). |
Ah, ok. I was thinking I made typo somewhere. I completely agree that if reading update would cause 1ms GC sweeps, it would be unacceptable. It would be visible in inspector js profiler. |
Cool. Glad we're on the same page. |
The "readings object" pattern across different Sensors API, will lead to different sensors of allocating various objects. Those might be a from single double (couple of bytes) to large objects, similar to touch events or mouse events. Naive way to store short-term or even long term history, would be by storing reading objects. Following such pattern this would lead to significant GC for sensors that have more than just few doubles, especially with referenced targets or more complex objects. This would lead to much bigger Retained Sizes associated with readings objects that keep references such as for DOM targets and other things. By having no readings object, there is no association cost and no way to store those readings objects. So if developer need to store history - then he stores what he needs. There is no multiple ways to go about it, it is straight forward with no misinterpretations. This practice avoids that retained memory size paths, and defines good basis for API design which has no different use paths and does not require to learn certain way to avoid issue in case if they occur, which in big applications is hard to track because there are consequence of many many tangled allocations. @alexshalamov, I see you mostly "resistive" against those changes. But I couldn't find the actual motivation behind such resistance. Does no "readings object" path makes things much worse in your opinion, and what are those "worse" things? Is it technical reasoning or more of a conceptual/design. I can't see how "readings object" API design is better to be honest, listing reasoning above - it leads to multiple ways of doing things (referencing readings object or taking data from reading objects), and leads to Retained Size issues, especially with other Sensor API's that might have much more data provided. And especially taking in account potentially not all readings data needs to be changed on each reading. Could you please reason the "resistance" you show? |
@Maksims I'm not resisting anything, I'm for simpler and faster API and its implementation. If you would check the implementation in chromium you will understand that we have the same goal. I'm +1 for the removal of the intermediate reading object, it reduced implementation complexity in chromium as well as API surface area. |
@pozdnyakov We would need to investigate and re-measure performance impact when we have batching mode. Moving to Level 2. |
On the list, @Maksims writes:
The text was updated successfully, but these errors were encountered: