Skip to content
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

Firefox's privacy.resistFingerprinting=true triggers assert in emscripten example #3644

Open
DonKult opened this issue Dec 7, 2020 · 7 comments

Comments

@DonKult
Copy link
Contributor

DonKult commented Dec 7, 2020

Version/Branch of Dear ImGui:

Version: 1.80 WIP (17906)
Branch: master

Back-end/Renderer/Compiler/OS

Back-ends: emscripten_opengl3
Compiler: emcc 2.0.8 (using clang 11)
Operating System: Debian unstable

My Issue/Question:

Compiling the example application works just fine. It also runs just fine with Chromium (83.0.4103.116) and Firefox (83.0) in its default configuration, but if you are running Tor Browser or if you set privacy.resistFingerprinting in Firefox's about:config to true the example will crash on a failed assert after the first frame (so you get a still image of correctly rendered frame 0).

The assert in question is in imgui.cc:6917:
IM_ASSERT((g.IO.DeltaTime > 0.0f || g.FrameCount == 0) && "Need a positive DeltaTime!");

The reason is that g.IO.DeltaTime will be zero for most frames and only sometimes be 0.1f instead of reporting constantly a very precise timing of around 0.016f as the resolution for time is reduced (and jitter added). Some technical details can be found in the bugzilla entry and there also seems to be an option for sites requiring high precision (have not tried though).

I am not quite sure what this assert is guarding against, but as 0.f is a sort-of positive value as well that could perhaps be changed to >=. Disabling this assert at least lets the demo run just fine: I presume some time-based animations and such will be a bit jittery (e.g. Widgets > Plot Widgets > Lines) if you are sensitive to it, but that seems acceptable compared to not being able to use an application at all in this configuration.

@ocornut
Copy link
Owner

ocornut commented Dec 9, 2020

Hello,

Thanks for the report.

I am not quite sure what this assert is guarding against,

We need correct timing, it's used for dozens of things, double-click time, keyboard repeat rates, unlock delays after mouse wheeling, hovered idioms, drag to hold, garbage collection policies, any form of animation (fading in/out of dimming overlays). This is what the assert is guarding against. Running with series of dt==0.0f followed by large dt value is going to create hard to notice issues which might be even worst.

If we have no way to bypass that, I think it should be something the backends support to automatically detect that situation on emscripten and turn the uneven time values into a smoothed set. Then it becomes the backend responsibility to handle it. If the code is obvious I don't mind adding that code in core imgui as an opt-in thing or an Emscripten-only thing, but someone has to write that code :)

there also seems to be an option for sites requiring high precision (have not tried though).

Would you know how to enable those headers in the example Emscripten applications?
That sounds like a better solution for now?

@ocornut
Copy link
Owner

ocornut commented Dec 9, 2020

Note that this code could technically be inserted between ImGui_ImplXXX_NewFrame() and ImGui::NewFrame()

Basically it needs to:

  • detect that case and smooth it up
  • never let a 0.0 go through (so maybe for the first few frames turn that into 0.0001 until the algorithm can be warmed up)

Could you log the DeltaTime values you get from Firefox, from frame 0 and paste them here? That would help deciding on the right way to implement that smoothing algorithm, if we can't use the Cross-Origin-Opener-Policy thing instead.

@DonKult
Copy link
Contributor Author

DonKult commented Dec 11, 2020

Thanks for having a look. I tried playing with the demo some more and you are right, there are funny issues: Doubleclick works, (positive) key repeat as well, but backspace tends to eat a couple characters in text inputs on a single press.

Log of the first 61 frames
DeltaTime: 0.016667  FrameCount:  0
DeltaTime: 0.000000  FrameCount:  1
DeltaTime: 0.000000  FrameCount:  2
DeltaTime: 0.100000  FrameCount:  3 X
DeltaTime: 0.000000  FrameCount:  4
DeltaTime: 0.000000  FrameCount:  5
DeltaTime: 0.000000  FrameCount:  6
DeltaTime: 0.100000  FrameCount:  7 X
DeltaTime: 0.000000  FrameCount:  8
DeltaTime: 0.000000  FrameCount:  9
DeltaTime: 0.100000  FrameCount: 10 X
DeltaTime: 0.000000  FrameCount: 11
DeltaTime: 0.000000  FrameCount: 12
DeltaTime: 0.000000  FrameCount: 13
DeltaTime: 0.000000  FrameCount: 14
DeltaTime: 0.000000  FrameCount: 15
DeltaTime: 0.000000  FrameCount: 16
DeltaTime: 0.000000  FrameCount: 17
DeltaTime: 0.000000  FrameCount: 18
DeltaTime: 0.000000  FrameCount: 19
DeltaTime: 0.000000  FrameCount: 20
DeltaTime: 0.100000  FrameCount: 21 X
DeltaTime: 0.000000  FrameCount: 22
DeltaTime: 0.000000  FrameCount: 23
DeltaTime: 0.000000  FrameCount: 24
DeltaTime: 0.000000  FrameCount: 25
DeltaTime: 0.000000  FrameCount: 26
DeltaTime: 0.100000  FrameCount: 27 X
DeltaTime: 0.100000  FrameCount: 28 X
DeltaTime: 0.000000  FrameCount: 29
DeltaTime: 0.000000  FrameCount: 30
DeltaTime: 0.000000  FrameCount: 31
DeltaTime: 0.000000  FrameCount: 32
DeltaTime: 0.000000  FrameCount: 33
DeltaTime: 0.000000  FrameCount: 34
DeltaTime: 0.100000  FrameCount: 35 X
DeltaTime: 0.000000  FrameCount: 36
DeltaTime: 0.000000  FrameCount: 37
DeltaTime: 0.000000  FrameCount: 38
DeltaTime: 0.000000  FrameCount: 39
DeltaTime: 0.000000  FrameCount: 40
DeltaTime: 0.000000  FrameCount: 41
DeltaTime: 0.000000  FrameCount: 42
DeltaTime: 0.000000  FrameCount: 43
DeltaTime: 0.000000  FrameCount: 44
DeltaTime: 0.100000  FrameCount: 45 X
DeltaTime: 0.000000  FrameCount: 46
DeltaTime: 0.100000  FrameCount: 47 X
DeltaTime: 0.000000  FrameCount: 48
DeltaTime: 0.000000  FrameCount: 49
DeltaTime: 0.000000  FrameCount: 50
DeltaTime: 0.000000  FrameCount: 51
DeltaTime: 0.000000  FrameCount: 52
DeltaTime: 0.000000  FrameCount: 53
DeltaTime: 0.000000  FrameCount: 54
DeltaTime: 0.000000  FrameCount: 55
DeltaTime: 0.100000  FrameCount: 56 X
DeltaTime: 0.000000  FrameCount: 57
DeltaTime: 0.100000  FrameCount: 58 X
DeltaTime: 0.000000  FrameCount: 59
DeltaTime: 0.000000  FrameCount: 60

The value of frame 0 is stable across tests. Ignoring this the following 60 frames will be roughly 50 frames with 0.f delta and 10 with 0.1f. Roughly as the frame count between impacts is not predictable (by design), so it can be the next frame or (empirical) 11 more until the next impact. In other words from a DeltaTime view you are somewhere between 0.2f in the past/future.

The emscripten example uses the SDL backend at the moment (I expect it to be similar with other backends), which calculates DeltaTime via SDL_GetPerformanceCounter() which is what is kept (relatively) stable. So, it seems with the really dumb hack (equal-compare on floats & perfectly stable 60 frames for everyone) of adding

    if (io.DeltaTime == 0.f || io.DeltaTime == 0.1f)
        io.DeltaTime = 1.f / 60.f;

there I can workaround my problem for now until someone (= hopefully not me) feels bored enough to investigate a proper solution – I would presume keeping tabs on "accumulated diff time" vs. "real time" is more promising than trying the headers approach as headers tend to be hard to set in the wild.

@ocornut
Copy link
Owner

ocornut commented Jan 24, 2023

Sorry for late answer. Recently opened #6114 tried to tackles the same issue. (EDIT: not exactly, see post below)

Some suggestions:

  • Investigate how the rest of the world is dealing with this? Surely, every real-time 3D application must be dealing with the same issue on Emscripten.
  • Put responsibility on Application to overwrite io.DeltaTime after calling the backend's NewFrame() ? If the application mainloop aims to run at a known framerate, it could specify it? Or it could approximate an rolling average framerate and feed that to dear imgui?

Starting from a helper, e.g.

template<typename TYPE>
struct ImMovingAverage
{
    // Internal Fields
    ImVector<TYPE>  Samples;
    TYPE            Accum;
    int             Idx;
    int             FillAmount;

    // Functions
    ImMovingAverage()               { Accum = (TYPE)0; Idx = FillAmount = 0; }
    void    Init(int count)         { Samples.resize(count); memset(Samples.Data, 0, (size_t)Samples.Size * sizeof(TYPE)); Accum = (TYPE)0; Idx = FillAmount = 0; }
    void    AddSample(TYPE v)       { Accum += v - Samples[Idx]; Samples[Idx] = v; if (++Idx == Samples.Size) Idx = 0; if (FillAmount < Samples.Size) FillAmount++;  }
    TYPE    GetAverage() const      { return Accum / (TYPE)FillAmount; }
    int     GetSampleCount() const  { return Samples.Size; }
    bool    IsFull() const          { return FillAmount == Samples.Size; }
};

Applications feeds delta-time extrapolated from SDL_GetPerformanceCounter() calls, and overwrites io.DeltaTime with the current moving average?

Arguably we could decide to move the responsibility of it to either the backend, either even dear imgui.. it's not hard to implement per-se, but the problem is that "enabling" the feature solely on #ifdef __EMSCRIPTEN__ is making a hard assumption that io.DeltaTime will always be janky and unreliable on Emscripten...... So maybe that feature can be added to core-Dear ImGui but guarded by a config flags.

@ocornut
Copy link
Owner

ocornut commented Jan 24, 2023

My bad, I forgot that privacy.resistFingerprinting=true is NOT the default, putting less pressure on that.
The issue in #6114 is a rarer occurrence.

@ocornut
Copy link
Owner

ocornut commented Jan 25, 2023

With 0749061 we made the core lib tolerate zero io.DeltaTime on Emscripten.
Effectively, this would make your resistFingerprinting=true case more functional although janky.
My opinion stated in #6114 is that this is better than nothing (and similar to your initial suggestion)
BUT we should aim for backends and/or backend to provide a smoothed DeltaTime in that situation.

@PathogenDavid
Copy link
Contributor

  • Investigate how the rest of the world is dealing with this? Surely, every real-time 3D application must be dealing with the same issue on Emscripten.

It's not an Emscripten issue, it's a browser issue. Browser software is inherently untrusted. Some big brain security flaws rely on having an accurate, high-resolution timer. As such browsers don't provide one (at least not to the standard most desktop software is used to having.)

The specification for high-resolution time specifically allows for reduced precision and accuracy. The only guarantee according to the spec is that you get is that the timer will be monotonic and have a resolution of at least 1 millisecond.

For example, these days Firefox is only accurate to the millisecond, regardless of the resistFingerprinting setting. (See here or just run performance.now() in the dev console and observe that it's always a round number.) -- With resistFingerprinting the user further override the resolution and violate the spec to make it as low as 100 ms.

(The documentation makes it sound like 100 ms is the default, but I believe that's no longer the case. The default for privacy.resistFingerprinting.reduceTimerPrecision.microseconds on Firefox 108 on Windows is 1 ms, so all it really does to the timer is add jitter.)

In general I think it's probably better to just accept browsers are janky and users fiddling with advanced config options makes them even more janky. We should just tolerate a delta time of 0 since really the only thing guaranteed by the spec is that it won't be negative.


One thing that does still puzzle me though is that the Emscripten main loop is typically dispatched using requestAnimationFrame, which is typically dispatched at the monitor's refresh rate. As such I'd always expect a measurable delta time here (except maybe when the user has resistFingerprinting with an absurd precision override.)

(Emphasis on "typically" though, the spec makes no specific requirements. Even the basic MDN example for requestAnimationFrame has logic to ignore duplicate timestamps, there might be a common quirk/edge case that causes them.)

(Another possibility is that Emscripten might be scheduling the next dispatch at the start of the requestAnimationFrame handler. I can definitely see how that could potentially cause duplicate timestamps if the tab goes to sleep or gets preempted at the wrong time, which could lead to two dispatches appearing to happen within the same millisecond as the browser tries to catch up.)

I'd have to dig into the Emscripten internals, but I wonder if it tries to "emulate" higher update rates when you request them. I know setTimeout is capable of exceeding the update rate of requestAnimationFrame.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants