Implement React v18 from Scratch Using WASM and Rust - [14] Implement Scheduler

ayou - May 27 - - Dev Community

Based on 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

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 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.

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

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

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. 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:

...
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;
    }
}
...
Enter fullscreen mode Exit fullscreen mode

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

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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()>);
Enter fullscreen mode Exit fullscreen mode

Even changing it to a closure would not work:

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

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:

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);
    };
}
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .