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

Avoid unnecessary babel polyfills and fix problem with nested worklets. #882

Merged
merged 6 commits into from
Jun 12, 2020

Conversation

Szymon20000
Copy link
Contributor

@Szymon20000 Szymon20000 commented Jun 10, 2020

The Typescript fix revealed another issue with overzealous babel polyfills. The pull-request solves two most important problems caused by @babel/plugin-transform-object-assign and @babel/plugin-transform-parameters.

Fixes problem with nested worklets by adding 'global' to our blacklist and providing dummy __reanimatedWorkletInit implementation on UI side.

Additional info:

1 "Object.assign"
Babel plugin "@babel/plugin-transform-object-assign" transforms Object.assign() call into (0, _extend.default)() where _extend is a module with Object.assign polyfill. Because the plugin do that we capture _extend object and bad things happen. This pr renames "Object" to "Object__DO_NOT_TRANSFOR" in each Object.assign call on entry and reverts this change on exit.

2 "function(...args)"
Babel plugin @babel/plugin-transform-parameters transforms spread syntax and uses arguments instead. To prevent "arguments" variable from being captured the pr add it to the blacklist.

3 Nested Worklets problem

function out(easing) {
  'worklet';
  return t => {
    'worklet';
    return 1 - easing(1 - t);
  };
}

->

var out = function () {
    var _f = function _f(easing) {
      return function () {
        var _f = function _f(t) {
          return 1 - easing(1 - t);
        };

        _f._closure = {
          easing: easing
        };
        _f.asString = "function(t){const{easing}=this._closure;{return 1-easing(1-t);}}";
        _f.__workletID = 615968805831595100;

        global.__reanimatedWorkletInit(_f);

        return _f;
      }();
    };

    _f._closure = {
      global: global
    };
    _f.asString = "function(easing){const{global}=this._closure;{return function(){var _f=function _f(t){return 1-easing(1-t);};_f._closure={easing:easing};_f.asString=\"function(t){const{easing}=this._closure;{return 1-easing(1-t);}}\";_f.__workletID=615968805831595100;global.__reanimatedWorkletInit(_f);return _f;}();}}";
    _f.__workletID = 571397156231203700;

    global.__reanimatedWorkletInit(_f);

    return _f;
  }();

As we can see global is captured.

@Szymon20000 Szymon20000 requested a review from kmagiera June 10, 2020 12:30
plugin.js Outdated
enter(path) {
if (path.get('callee').matchesPattern('Object.assign')) {
// @babel/plugin-transform-object-assign
path.node.callee.object.name = 'random_temp_name';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename to something more descriptive. For example: "Object__DO_NOT_TRANSFORM"

plugin.js Outdated
},
FunctionExpression: {
exit(path) {
processIfWorkletNode(t, path);
}
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put all of these under a single visitor the way it used to be:

'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression': {
   enter(path) { ... }

Copy link
Contributor Author

@Szymon20000 Szymon20000 Jun 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@kmagiera
Copy link
Member

The code looks good, just see a few minor comment. Would be great to improve the description of the PR such that:

  • we can understand from it what "nested worklet" problem was? Maybe we can link to some issue? Why we don't want nested worklets and if we don't want then why? I can imagine a usecase where I'd like to have nested worklets, then I could run the parent worklet from RN thread and expect it to return a worklet that can be executed on the UI thread.
  • Also would be great to add a brief description on what is the problem with object assign and how this PR fixes it
  • The same for the problem with parameters plugin – how this PR addressed this problem?

@Szymon20000 Szymon20000 requested a review from kmagiera June 10, 2020 13:59
@kmagiera
Copy link
Member

Re pt 3 I think we should transform all the worklets. In the example you provided the way out function can be used is should support the following usecases:

  1. out is called on the JS side, and the result is passed to UI and expected to be a worklet
  2. out is called on the UI side and the result is used on the JS side

If we only transform out and not the inner worklet, we wouldn't be able to support 1st usecase. If we only transformed the inner and not outer we wouldn't be able to support 2nd worklet.

@kmagiera kmagiera merged commit e8316fd into master Jun 12, 2020
@kmagiera kmagiera deleted the @szymon/fix_plugin_ branch June 12, 2020 14:29
@tomekzaw tomekzaw mentioned this pull request Dec 6, 2022
4 tasks
piaskowyk pushed a commit that referenced this pull request Dec 8, 2022
## Summary

This PR removes a piece of code in `RuntimeDecorator` that used to overwrite `global` property of the global JS object with a dummy JS object which was a regular JS object filled only with Reanimated-specific values.

## Motivation

I tried to confirm that Hermes was indeed enabled on the UI context with the following snippet:
```ts
runOnUI(() => {
  'worklet';
  console.log('HermesInternal' in global); // should return true
})();
```
However, it returned `false`, despite the fact that a breakpoint on `facebook::hermes::makeHermesRuntime` was hit.

Then I noticed that we call `RuntimeDecorator::decorateRuntime(rt, "UI");` which overwrites `global` property in the UI context with a "dummy global", effectively causing `global.HermesInternal` to be `undefined`. 

## Context

The concept of dummy global was introduced in the following PR:

* #882

There was one attempt to remove dummy global in the past but the change was immediately reverted in f846704:

* #2253

As @wkozyra95 stated:

> removed dummyGlobal and make `global.global` point to itself. I don't necessarily need that change to make it work, but the current behavior of global value seems incorrect

## Changes

One might wonder why not just remove these three lines:

https://github.com/software-mansion/react-native-reanimated/blob/074cac02d2ef503e7897ef63560d3b8514a8e350/Common/cpp/Tools/RuntimeDecorator.cpp#L33-L35

I'm glad you asked. The answer is that is simply doesn't work:

<img width="250" alt="Zrzut ekranu 2022-12-6 o 11 27 08" src="https://user-images.githubusercontent.com/20516055/205896462-1616af17-070d-43a9-9231-1bf4c6cb3e24.png">

That's because in many places we use `global.foo` syntax where `global` most likely represents the `global` property of the global object, not the global object itself (it's complicated).

The solution is to add the global JS object as the `global` property of itself so that `global.global === global`.

Obviously, it's like a reference cycle but hopefully the runtime knows how to clean itself once it gets terminated.

## What about ...?

### Overriding runtime global

Snippet:

```tsx
import React from 'react';
import { runOnUI } from 'react-native-reanimated';

export default function App() {
  // JS
  console.log('HermesInternal' in global);

  runOnUI(() => {
    'worklet';
    // UI
    console.log('HermesInternal' in global);
  })();

  return <></>;
}
```

Before:

![before2](https://user-images.githubusercontent.com/20516055/206486688-d6ac80fb-66b7-4a9d-8134-64895efd7c0f.png)

After:

![after2](https://user-images.githubusercontent.com/20516055/206486664-1fb9e786-9e42-45c2-bb4b-1131e0a9f616.png)

### Runtime deallocation

After the changes, the runtime is deallocated on app reload:

<img width="1103" alt="dealloc" src="https://user-images.githubusercontent.com/20516055/205902815-8087572d-33ff-4909-901d-39458f68ac73.png">

### Capturing global in worklets

Should work the same way since `plugin.js` hasn't been changed.

Input (from #882):

```ts
function out(easing) {
  'worklet';
  return t => {
    'worklet';
    return 1 - easing(1 - t);
  };
}
```

Command: `npx babel file.js`

Output (after formatting):

```js
var out = (function () {
  var _f = function _f(easing) {
    return (function () {
      var _f = function _f(t) {
        return 1 - easing(1 - t);
      };
      _f._closure = { easing: easing };
      _f.asString =
        'function _f(t) {\n  const {\n    easing\n  } = this._closure;\n  return 1 - easing(1 - t);\n}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBRVNBLFNBQUNDLEVBQURELENBQUNBLENBQURBLEVBQUs7QUFBQTtBQUFBO0FBQUE7QUFFVixTQUFPLElBQUlFLE1BQU0sQ0FBQyxJQUFJRixDQUFMLENBQWpCO0FBRktBIiwibmFtZXMiOlsidCIsIl9mIiwiZWFzaW5nIl0sInNvdXJjZXMiOlsiL1VzZXJzL3RvbWVremF3L1JOT1MvU2hhcmVhYmxlUmV3cml0ZS9yZWFjdC1uYXRpdmUtcmVhbmltYXRlZC9maWxlLmpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIG91dChlYXNpbmcpIHtcbiAgJ3dvcmtsZXQnO1xuICByZXR1cm4gdCA9PiB7XG4gICAgJ3dvcmtsZXQnO1xuICAgIHJldHVybiAxIC0gZWFzaW5nKDEgLSB0KTtcbiAgfTtcbn0iXX0=';
      _f.__workletHash = 8053949848116;
      _f.__location =
        '/Users/tomekzaw/RNOS/ShareableRewrite/react-native-reanimated/file.js (3:9)';
      return _f;
    })();
  };
  _f._closure = {};
  _f.asString =
    "function out(easing) {\n  return function (t) {\n    'worklet';\n\n    return 1 - easing(1 - t);\n  };\n}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQUE7QUFFRSxTQUFPQSxhQUFLO0FBQ1Y7O0FBQ0EsV0FBTyxJQUFJQyxNQUFNLENBQUMsSUFBSUQsQ0FBTCxDQUFqQjtBQUZGO0FBRkYiLCJuYW1lcyI6WyJ0IiwiZWFzaW5nIl0sInNvdXJjZXMiOlsiL1VzZXJzL3RvbWVremF3L1JOT1MvU2hhcmVhYmxlUmV3cml0ZS9yZWFjdC1uYXRpdmUtcmVhbmltYXRlZC9maWxlLmpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIG91dChlYXNpbmcpIHtcbiAgJ3dvcmtsZXQnO1xuICByZXR1cm4gdCA9PiB7XG4gICAgJ3dvcmtsZXQnO1xuICAgIHJldHVybiAxIC0gZWFzaW5nKDEgLSB0KTtcbiAgfTtcbn0iXX0=";
  _f.__workletHash = 3984642990765;
  _f.__location =
    '/Users/tomekzaw/RNOS/ShareableRewrite/react-native-reanimated/file.js (1:0)';
  return _f;
})();
```

Conclusion: `global` is not captured (as `_f._closure = {};`), which is correct.

### `gc`

Snippet:

```tsx
import React from 'react';
import { runOnUI } from 'react-native-reanimated';

export default function App() {
  // JS
  console.log(gc);
  console.log(gc());
  console.log(global.gc);
  console.log(global.gc());

  runOnUI(() => {
    'worklet';
    // UI
    console.log(gc);
    // console.log(gc()); // Tried to synchronously call a non-worklet function on the UI thread.
    console.log(global.gc);
    console.log(global.gc());
  })();

  return <></>;
}
```

Before:

![before](https://user-images.githubusercontent.com/20516055/205899206-5736176c-3568-4697-bc09-c97ff6d67405.png)

After:

![after](https://user-images.githubusercontent.com/20516055/205899489-491357fe-e0f3-4ab9-8102-120f7e864383.png)

## Test plan

Just check the above test cases one-by-one.

## TODO

- [ ] check if runtime deallocates on reload (set a breakpoint in `~JSCRuntime`)
- [ ] check if `global` is not in closure
- [ ] check if `gc` works
- [ ] check if expo-gl works
fluiddot pushed a commit to wordpress-mobile/react-native-reanimated that referenced this pull request Jun 5, 2023
## Summary

This PR removes a piece of code in `RuntimeDecorator` that used to overwrite `global` property of the global JS object with a dummy JS object which was a regular JS object filled only with Reanimated-specific values.

## Motivation

I tried to confirm that Hermes was indeed enabled on the UI context with the following snippet:
```ts
runOnUI(() => {
  'worklet';
  console.log('HermesInternal' in global); // should return true
})();
```
However, it returned `false`, despite the fact that a breakpoint on `facebook::hermes::makeHermesRuntime` was hit.

Then I noticed that we call `RuntimeDecorator::decorateRuntime(rt, "UI");` which overwrites `global` property in the UI context with a "dummy global", effectively causing `global.HermesInternal` to be `undefined`. 

## Context

The concept of dummy global was introduced in the following PR:

* software-mansion#882

There was one attempt to remove dummy global in the past but the change was immediately reverted in f846704:

* software-mansion#2253

As @wkozyra95 stated:

> removed dummyGlobal and make `global.global` point to itself. I don't necessarily need that change to make it work, but the current behavior of global value seems incorrect

## Changes

One might wonder why not just remove these three lines:

https://github.com/software-mansion/react-native-reanimated/blob/074cac02d2ef503e7897ef63560d3b8514a8e350/Common/cpp/Tools/RuntimeDecorator.cpp#L33-L35

I'm glad you asked. The answer is that is simply doesn't work:

<img width="250" alt="Zrzut ekranu 2022-12-6 o 11 27 08" src="https://user-images.githubusercontent.com/20516055/205896462-1616af17-070d-43a9-9231-1bf4c6cb3e24.png">

That's because in many places we use `global.foo` syntax where `global` most likely represents the `global` property of the global object, not the global object itself (it's complicated).

The solution is to add the global JS object as the `global` property of itself so that `global.global === global`.

Obviously, it's like a reference cycle but hopefully the runtime knows how to clean itself once it gets terminated.

## What about ...?

### Overriding runtime global

Snippet:

```tsx
import React from 'react';
import { runOnUI } from 'react-native-reanimated';

export default function App() {
  // JS
  console.log('HermesInternal' in global);

  runOnUI(() => {
    'worklet';
    // UI
    console.log('HermesInternal' in global);
  })();

  return <></>;
}
```

Before:

![before2](https://user-images.githubusercontent.com/20516055/206486688-d6ac80fb-66b7-4a9d-8134-64895efd7c0f.png)

After:

![after2](https://user-images.githubusercontent.com/20516055/206486664-1fb9e786-9e42-45c2-bb4b-1131e0a9f616.png)

### Runtime deallocation

After the changes, the runtime is deallocated on app reload:

<img width="1103" alt="dealloc" src="https://user-images.githubusercontent.com/20516055/205902815-8087572d-33ff-4909-901d-39458f68ac73.png">

### Capturing global in worklets

Should work the same way since `plugin.js` hasn't been changed.

Input (from software-mansion#882):

```ts
function out(easing) {
  'worklet';
  return t => {
    'worklet';
    return 1 - easing(1 - t);
  };
}
```

Command: `npx babel file.js`

Output (after formatting):

```js
var out = (function () {
  var _f = function _f(easing) {
    return (function () {
      var _f = function _f(t) {
        return 1 - easing(1 - t);
      };
      _f._closure = { easing: easing };
      _f.asString =
        'function _f(t) {\n  const {\n    easing\n  } = this._closure;\n  return 1 - easing(1 - t);\n}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBRVNBLFNBQUNDLEVBQURELENBQUNBLENBQURBLEVBQUs7QUFBQTtBQUFBO0FBQUE7QUFFVixTQUFPLElBQUlFLE1BQU0sQ0FBQyxJQUFJRixDQUFMLENBQWpCO0FBRktBIiwibmFtZXMiOlsidCIsIl9mIiwiZWFzaW5nIl0sInNvdXJjZXMiOlsiL1VzZXJzL3RvbWVremF3L1JOT1MvU2hhcmVhYmxlUmV3cml0ZS9yZWFjdC1uYXRpdmUtcmVhbmltYXRlZC9maWxlLmpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIG91dChlYXNpbmcpIHtcbiAgJ3dvcmtsZXQnO1xuICByZXR1cm4gdCA9PiB7XG4gICAgJ3dvcmtsZXQnO1xuICAgIHJldHVybiAxIC0gZWFzaW5nKDEgLSB0KTtcbiAgfTtcbn0iXX0=';
      _f.__workletHash = 8053949848116;
      _f.__location =
        '/Users/tomekzaw/RNOS/ShareableRewrite/react-native-reanimated/file.js (3:9)';
      return _f;
    })();
  };
  _f._closure = {};
  _f.asString =
    "function out(easing) {\n  return function (t) {\n    'worklet';\n\n    return 1 - easing(1 - t);\n  };\n}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQUE7QUFFRSxTQUFPQSxhQUFLO0FBQ1Y7O0FBQ0EsV0FBTyxJQUFJQyxNQUFNLENBQUMsSUFBSUQsQ0FBTCxDQUFqQjtBQUZGO0FBRkYiLCJuYW1lcyI6WyJ0IiwiZWFzaW5nIl0sInNvdXJjZXMiOlsiL1VzZXJzL3RvbWVremF3L1JOT1MvU2hhcmVhYmxlUmV3cml0ZS9yZWFjdC1uYXRpdmUtcmVhbmltYXRlZC9maWxlLmpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIG91dChlYXNpbmcpIHtcbiAgJ3dvcmtsZXQnO1xuICByZXR1cm4gdCA9PiB7XG4gICAgJ3dvcmtsZXQnO1xuICAgIHJldHVybiAxIC0gZWFzaW5nKDEgLSB0KTtcbiAgfTtcbn0iXX0=";
  _f.__workletHash = 3984642990765;
  _f.__location =
    '/Users/tomekzaw/RNOS/ShareableRewrite/react-native-reanimated/file.js (1:0)';
  return _f;
})();
```

Conclusion: `global` is not captured (as `_f._closure = {};`), which is correct.

### `gc`

Snippet:

```tsx
import React from 'react';
import { runOnUI } from 'react-native-reanimated';

export default function App() {
  // JS
  console.log(gc);
  console.log(gc());
  console.log(global.gc);
  console.log(global.gc());

  runOnUI(() => {
    'worklet';
    // UI
    console.log(gc);
    // console.log(gc()); // Tried to synchronously call a non-worklet function on the UI thread.
    console.log(global.gc);
    console.log(global.gc());
  })();

  return <></>;
}
```

Before:

![before](https://user-images.githubusercontent.com/20516055/205899206-5736176c-3568-4697-bc09-c97ff6d67405.png)

After:

![after](https://user-images.githubusercontent.com/20516055/205899489-491357fe-e0f3-4ab9-8102-120f7e864383.png)

## Test plan

Just check the above test cases one-by-one.

## TODO

- [ ] check if runtime deallocates on reload (set a breakpoint in `~JSCRuntime`)
- [ ] check if `global` is not in closure
- [ ] check if `gc` works
- [ ] check if expo-gl works
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants