Skip to content

Instantly share code, notes, and snippets.

@nin-jin
Last active December 6, 2023 07:02
Show Gist options
  • Save nin-jin/5408ef8f16f43f1b4fe9cbcea577aac6 to your computer and use it in GitHub Desktop.
Save nin-jin/5408ef8f16f43f1b4fe9cbcea577aac6 to your computer and use it in GitHub Desktop.

Fibers

Motivation

Pausable stackful coroutines (fibers) give ability to write simple but responsive and concurrent code.

It can be used for:

  • Automatic pause long task every 16 ms to ensure 60fps.
  • Concurrent execution of different tasks (not serial).
  • Abort incomplete task (with subtasks) as reaction on event.
  • Write simple (synchronous) code.
  • Improve performance because synchronous code can be better optimized by JIT.
  • Call synchronous API (like Array:reduce) without loosing of concurrency.

API

// Fiber - lightweight thread, that has separate call stack.
// Multiple fibers concurrently executes in one system thread.
declare class Fiber< Result > extends Promise< Result > {

	// Fiber that are executing now.
	static current : Fiber< any > | null

	// Freezes current fiber until promise will be resolved.
	// After, resumes fiber and returns result or rethrows an exception if promise is rejected.
	// Throws NotInFiberError if called outside any fiber.
	static wait< Result >( promise : PromiseLike< Result > ) : Result

	// Executes function in separate fiber
	static run( task : ()=> Result ) : Fiber< Result >

	// Abort execution.
	// Do nothing if is already completed.
	abort : ()=> void

}

Usage examples

Quantizing

Function that returns control to main loop every 8 ms:

let deadline = Date.now() + 8

function quant() {

	// Do nothing if fibers isn't supported 
	if( typeof Fiber !== 'function' ) return

	// Do nothing if called outside any fiber
	if( !Fiber.current ) return

	// Do nothing until deadline
	if( Date.now() < deadline ) return

	// Stop current fiber until next frame
	Fiber.wait( new Promise( requestAnimationFrame ) )

	// already in next animation frame
	dealine = Date.now() + 8

}

JSON processing

Quantized json parsing that don't block event loop more than ~8 ms independent on string size:

json_parse( str ) : any {
	return JSON.parse( str , ( key , value )=> ( quant() , value ) )
}

Usage:

import { promisify } from 'util'
import { readFile } from 'fs'
const readFileAsync = promisify( readFile )

// Some synchronous task
function log_config() {

	// Load large JSON
	const buffer = Fiber.wait( fs.readFileASync( 'config.json' ) )

	// Parse json without long thread blocking if can
	const json = json_parse( buffer )

	// Print json to console
	console.log( json )
}

// Spawn fiber
Fiber.run( log_config )

60 FPS rendering

Very huge rendering (over 9000ms):

// Render large tree.
function render_tree( count ) {

	// [ 0 , 1 , 2 , ... ]
	const numbers = [ ... Array( count ) ].map( ( _ , i ) => i )

	// [ <iframe/> , <iframe/> , ... ]
	const leafs = render_branch( document.body , numbers )

	console.log( leafs )
}

// Render branch.
function render_branch( parent , numbers ) {

	// Render element for branch.
	const el = document.createElement( 'div' )
	parent.appendChild( el )
	
	// Render few leafs and return its.
	if( numbers.length <= 5 ) return numbers.map( render_leaf.bind( el ) )

	// Split numbers to two buckets.
	const center = Math.floor( numbers.length / 2 )
	const left = numbers.slice( 0 , center )
	const right = numbers.slice( center )
	
	// Render sub branches and join returned leafs
	return [ ... render_branch( el , left ) , ... render_branch( el , right ) ]
	
}

function render_leaf( parent ) {
	
	// Ability to free main thread every 8 ms
	quant()
	
	// Do hard work (iframes are very expansive)
	const el = document.createElement( 'iframe' )
	parent.appendChild( el )
	
	return el
}

Start rendering in separate fiber:

let rendering = Fiber.run( ()=> render_tree( 1000 ) )

Abort old rendering and start new on event:

// On any external event
window.onmessage( count => {
  
	// Stop revious rendering
	rendering.abort()
	
	// Start new rendering
	rendering = Fiber.run( ()=> render_tree( count ) )
  
} )

Optional Fetch

Simple fetch wrapper:

function get_json( url : string ) : any {

	const controller = new AbortController();
	const signal = controller.signal;

	// Wait for server response
	const response = Fiber.wait( fetch( url , { signal } ) )

	// Wait for json parse
	const json = Fiber.wait( response.json() )

	return json	
}

Optional asynchrony in other code:

let cache : { beta : boolean }

function get_config() : typeof cache {

	// Fill cache if not exists 
	if( !cache ) cache = get_json( 'example.org' ).flags

	return cache
}

Fiber.run( ()=> {

	// Will suspend while htt requesting
	console.log( get_config() )

	// Instant returns data from cache
	console.log( get_config() )

} )

Compatibility

Async functions

// Async in fiber
function main() {
	const promise = fetch( '//example.org' )
	const response = Fiber.wait( promise )
	const json = Fiber.wait( response.json() )
	console.log( json )
}

const fiber = Fiber.run( main )
// Fiber in async
async function main2 () {
	console.log( 'start' )
	await fiber
	console.log( 'finish' )
})

main2()

Gracefull degradation

Some features can be transparently disabled if fibers isn't supported.

// Continue in next tick if possible
function tick() {

	// Do nothing if fibers isnt' supported
	if( typeof Fiber !== 'function' ) return

	// Do nothing if called outside any fiber
	if( !Fiber.current ) return

	// Wait few time
	Fiber.wait( new Promise( process.nextTick ) )

}

Limitations

Usage of global variables and finally block together should be changed to support execution in fibers

let context = null

function foo( callback1, callback2 ) {

	const prev = {}
	context = {}

	try {
		 // If `Fiber.wait` is called other `foo` execution can change context
		callback1()
		
		// Then we get wrong context there
		callback2()
		
	} finally {
		context = prev
	}
	
}
const context = new WeakMap

function foo( callback1, callback2 ) {

	// Every context is fiber bound
	const prev = context.get( Fiber.current )
	context.set( Fiber.current , {} )

	try {
		 // Other `foo` execution don't change our context
		callback1()
		
		// Then we get right context there
		callback2()
		
	} finally {
		context.set( Fiber.current , prev )
	}
	
}

Related Implementations

Other Languages

Stackfull vs stackless coroutines

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