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

yoga-layout as a component #1

Open
privatenumber opened this issue Mar 10, 2022 · 4 comments
Open

yoga-layout as a component #1

privatenumber opened this issue Mar 10, 2022 · 4 comments

Comments

@privatenumber
Copy link
Collaborator

yoga-layout offers a lot of features out of the box, but I'm curious if it's possible to move yoga-layout out of the core runtime and abstract it into a tree-shakable component (eg. Box/FlexBox) or a 2nd party component (separate package)?

For context, the main reasons why I want to move away from Ink are:

  • React hooks and data immutability is not ergonomic/intuitive to me
  • File size (largely due to yoga-layout)
  • Yoga-layout can be slow at rendering

Large filesize

yoga-layout-prebuilt alone is around 89.3 kB gzipped (379 kB minified). For reference, Vue 3 is 128 kB minified.

Currently, that means any CLI script that uses this tool will be at least 379 kB minified. This might deter users that are looking to create something simple.

I have two tools that rely on Ink right now, and they're both bloated because of this:

They don't actually do that much. And I don't think they even use the flexbox features. So ideally, I can get them down to less than 50 kB max and I was hoping moving to Vue for SFC compilation + treeshaking could help with that.

Use-cases

Let me know if I'm oversimplifying what yoga-layout does, but AFAIK it implements flexbox in any environment (eg. terminal). This sounds like a great feature, but I'm wondering how often this will be used.

Personally, I don't use any CLI tools that draw boxes, are full screen, need to align content with flexbox powers, etc. Most of the ones I use or make usually have simple and inline UI.

However, it is a great building block to offer. Especially as I read Ink's documentation, it seems like a big selling point to have as a core component. I'm curious if it we can still have it as a tree-shakable component? If the installation size were to deter users, possibly as a separate package?

Slow rendering speed

As I investigated this, I realized there are a few factors that impact rendering speed, and may not even be caused by yoga-layout.

I'll include it anyway since I still found it relevant and insightful for this project.

Rendering performance factors:

  1. The Terminal Emulator used. Native Terminal app is fastest, iTerm 2 is slow, and Hyper is even slower (although, less glitchy re-renders).

  2. Page size. Seems like the larger the render payload, the slower it is. This sounds expected, but it's dramatically slower, which I found unexpected. For example, if my Ink app renders 3 pages worth of data, each re-render is significantly slower than 1 page of data. In iTerm 2, it's glitches as well.

    As a side, I wondered about the possibility of a Page/Document component that overflows content and implements custom scroll so re-renders can be more efficient and less glitchy.

  3. Content change speed. I noticed my Ink app gets notably slower when using ink-spinner. Probably because it changes the render output on every tick, so the render cycle is constantly running.

I originally noticed slowness in my own scripts that use Tasuku. They were running dramatically slower when it was rendering a lot. (ie it was a lot faster to run the script when removing Tasuku). I eventually got an user reporting it: privatenumber/tasuku#5 (comment)

I created isolated experiments with multiple components running 1 second timeouts in parallel to check if it the script will exit in 1s.

Experiment

Code for Ink:

import React, { useState, useEffect } from 'react';
import {render, Text} from 'ink';

const Counter = () => {
	const [counter, setCounter] = useState(0);

	useEffect(() => {
		const timer = setTimeout(() => {
			setCounter(previousCounter => previousCounter + 1);
		}, 1000);

		return () => {
			clearInterval(timer);
		};
	}, []);

	return <Text color="green">{counter}</Text>;
};

render(
	React.createElement(
		React.Fragment,
		null,
		...Array.from(
			{ length: 300 },
			() => React.createElement(Counter),	
		),
	),
);

Result:

Terminal: 300 parallel => 4s

Note, because this is 300 1s timers in parallel, exit time should still happen in ~1s. This might be the real cost of yoga-layout. I would like to remove the factor of page size here by rendering them all in one line, but Ink seems to require all text to be wrapped in <Text>, which separates them into new lines.

npx esno src/ink.tsx  2.53s user 0.27s system 80% cpu 3.502 total

iTerm 2: 300 parallel => 7s

npx esno src/ink.tsx  5.55s user 0.39s system 86% cpu 6.858 total

Adding ink-spinner in the mix seems to slow down the script dramatically:

Terminal: 300 parallel => 36s

npx esno src/ink.tsx  34.79s user 0.94s system 99% cpu 35.809 total

iTerm 300 parallel => 46s (and major CPU lag)

npx esno src/ink.tsx  42.48s user 1.24s system 94% cpu 46.394 total
tasuku
import task from 'tasuku';

const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));

task.group(
	(task) => Array.from(
		{ length: 100 },
		() => task(
			'sleep 1s',
			() => sleep(1000),
		),
	),
	{
		concurrency: Infinity,
	},
);

Result: 100 parallel -> ~6s

npx esno src/index.ts  5.71s user 0.38s system 107% cpu 5.656 total

Result: 300 parallel -> ~ 2m 🤯

npx esno src/index.ts  86.37s user 2.81s system 77% cpu 1:54.47 total

As a separate optimization feature, I wonder how much faster only re-rendering changed lines would be. Alternatively, skipping line clearing and just overwriting instead (and only clearing the extra text at the end, if the new render is shorter than the previous).

@posva
Copy link
Collaborator

posva commented Mar 10, 2022

Wow, you went deep down the rabbit hole! 😄 I'm going to need some time to properly reply to everything, but to put it into a nutshell about yoga: it is, IMO, core to the experience of creating tuis and properly aligning things but it doesn't have to be yoga, it could be custom made too. It does need to be fast AFAIK. It could be something similar to the alignment system https://github.com/fdehau/tui-rs has.

I don't think yoga can be made tree shakable at the moment because a lot of the size computation logic comes from it
Creating a custom solution is definitely something interesting to dive into but it's not something I plan on doing yet, so if this is something you want to work on, I can give you hand for sure

@privatenumber
Copy link
Collaborator Author

Haha yeah, I've been thinking about this for a while!

I will look into how much work something custom would take, and whether that can integrate with yoga-layout as a component. I'm trying to wrap up my current projects now but will try to find some time over the weekend or next week.

@posva
Copy link
Collaborator

posva commented Mar 23, 2022

For context, the main reasons why I want to move away from Ink are:

  • React hooks and data immutability is not ergonomic/intuitive to me
  • File size (largely due to yoga-layout)
  • Yoga-layout can be slow at rendering

Ideally, I want to enable users to implement complex custom elements (they all start with tui:* and have a special treatment when rendering). If tui:box can be moved into some sort of plugin, that would be great. It would also mean users could add new properties to the base TuiNode class (like a DOMNode), so it's not something that simple to implement. IMO vue-termui isn't yet mature enough to introduce this kind of extendable API. It needs to be interesting to people first before introducing complex/advanced APIs to extend the base

I'm more concerned about the current status of yoga-layout and it being dropped by Facebook and the speed than the size. I think that a command line application that is under 1mb is reasonable but of course, being smaller is important too just not a priority yet, I would say.
A custom implementation of a flex-like algorithim would be the Cassowary constraint. tui-rs uses this implementation. One of the concers is of course speed, so using one in wasm is definitely an option.

Yoga does seem to add a few more features like absolute positioning. It's not really something I care about because it can glitch the render but it's nice to have.

To me, it seems, that what you personally are looking for is more logging than anything. Wouldn't something like https://github.com/ivanseidel/node-draftlog be better for tasuku? It only works per line, which is the main difference.

About speed:

  • A Pager component is something that could be added to the component library (like inputs, buttons, etc)
  • About the spinner, I thought about a specific Text component that only renders its line on change. To be able to do this, the text must have a max length and cannot span over multiple lines.

Maybe Ink is rendering too many times with React... With Vue, updates are grouped, I also buffer changes to output at max every 32ms.

@posva
Copy link
Collaborator

posva commented Mar 13, 2023

Note to myself: maybe worth checking if using something like https://github.com/DioxusLabs/taffy brings any improvements to size and maintanability

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

No branches or pull requests

2 participants