Implement React v18 from Scratch Using WASM and Rust - [14] Implement Scheduler
Tags: react, webassembly, rust
Published: 2024-05-27 02:59:03


> Based on [big-react](https://github.com/BetaSu/big-react)，I am going to implement React v18 core features from scratch using WASM and Rust.
>
> Code Repository：https://github.com/ParadeTo/big-react-wasm
>
> The tag related to this article：[v14](https://github.com/ParadeTo/big-react-wasm/tree/v14)

# Introduction to Scheduler

`Scheduler` is a package in React responsible for task scheduling, and it lays the groundwork for implementing time slicing. The upcoming `useEffect` will also utilize it, so in this article, we'll start by implementing a WASM (WebAssembly) version of the `Scheduler`.

For an introduction to `Scheduler`, you can refer to [this article](https://www.paradeto.com/2020/12/30/react-concurrent-1/) I wrote previously. Below is a brief introduction to its implementation.

`Scheduler` maintains two min-heaps: `timerQueue` and `taskQueue`. Tasks that are ready (`startTime` <= `currentTime`) are placed into the `taskQueue`, while tasks that are not yet ready (with `startTime` > `currentTime` achieved by passing in a `delay`) are placed into the `timerQueue`. For instance, in the example below, `task1` would be placed into the `taskQueue`, while `task2` would be placed into the `timerQueue`.

```js
const task1 = Scheduler.unstable_scheduleCallback(2, function func1() {
  console.log('2')
})

const task2 = Scheduler.unstable_scheduleCallback(
  1,
  function func2() {
    console.log('1')
  },
  {delay: 100}
)
```

Later on, a macro task is initiated through `MessageChannel` to process the tasks in the `taskQueue`. If the processing time exceeds 5ms, it will be interrupted, and then a new macro task will be started to continue processing the remaining tasks. This cycle repeats until all tasks in the heap are completed. Tasks in the `timerQueue` are periodically checked to see if they are ready. If they are, they are popped out in sequence and placed into the `taskQueue`.

The details of this update can be seen [here](https://github.com/ParadeTo/big-react-wasm/pull/13/files). Below, I will highlight and explain some key points.

# Implementation of Min Heap

To facilitate writing unit tests, a generic version of the min heap was implemented:

```rust
...
pub fn push<T: Ord>(heap: &mut Vec<T>, value: T) {
    heap.push(value);
    sift_up(heap, heap.len() - 1);
}
...
fn sift_up<T: Ord>(heap: &mut Vec<T>, mut index: usize) {
    while index != 0 {
        let parent = (index - 1) / 2;
        if heap[parent] <= heap[index] {
            break;
        }
        heap.swap(parent, index);
        index = parent;
    }
}
...
```

However, this generic `T` is constrained by `Ord`, requiring the implementation of the `Ord` trait, like this:

```rust
struct Task {
    id: u32
}

impl Eq for Task {}

impl PartialEq for Task {
    fn eq(&self, other: &Self) -> bool {
        self.id.cmp(&other.id) == Ordering::Equal
    }
}

impl PartialOrd for Task {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        return self.id.partial_cmp(&other.id);
    }
}

impl Ord for Task {
    fn cmp(&self, other: &Self) -> Ordering {
        self.partial_cmp(other).unwrap_or(Ordering::Equal)
    }
}
```

# static mut

The implementation of `Scheduler` defines a large number of `static mut`, leading to many unsafe code blocks in the code. Clearly, this is not a good practice, but the advantage of doing so is that the implementation is more similar to React's, which facilitates copying code. Moreover, another more important reason is that if we do not use `static mut`, but instead define a `Scheduler` struct and make these `static mut` as its attributes, other problems will be encountered.

For instance, when using `perform_work_until_deadline` as the callback function for a macro task, it would need to be changed to `self.perform_work_until_deadline`, and such a change would not compile:

```rust
pub fn schedule_perform_work_until_deadline() {
    let perform_work_closure =
        // Will fail to compile if it is changed to self.perform_work_until_deadline
        Closure::wrap(Box::new(perform_work_until_deadline) as Box<dyn FnMut()>);
```

Even changing it to a closure would not work:

```rust
pub fn schedule_perform_work_until_deadline() {
    let perform_work_closure =
        Closure::wrap(Box::new(|| self.perform_work_until_deadline()) as Box<dyn FnMut()>);
```

Therefore, it seems to be a necessary evil for the time being. However, by using unsafe to bypass Rust's safety checks, some strange behaviors can occur, such as the following example:

```rust
static mut MY_V: Vec<Task> = vec![];

#[derive(Debug)]
struct Task {
    name: String
}

fn peek<'a>(v: &'a mut Vec<Task>) -> &'a Task {
    &v[0]
}

fn pop<'a>(v: &'a mut Vec<Task>) -> Task {
    let t = v.swap_remove(0);
    t
}

fn main() {
    unsafe {
        MY_V = vec![Task {
            name: "ayou".to_string()
        }];

        let t = peek(&mut MY_V);

        // 1
        // pop(&mut MY_V);
        // 2
        let a = pop(&mut MY_V);

        println!("{:?}", t.name);
    };
}
```
Code 1 and Code 2 produce different outputs. Code 1 outputs `"\0\0\0\0"`, while Code 2 outputs normally, and the only difference between them is whether the return value is assigned to a variable or not.

As to why there is such a difference, it's not yet very clear. Fortunately, testing has revealed that there are no other issues for now, and next, we can proceed to implement `useEffect`.

Please kindly give me a star!