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

Adding tcell as a dependency delays startup time #764

Closed
asturur opened this issue Jan 6, 2025 · 10 comments
Closed

Adding tcell as a dependency delays startup time #764

asturur opened this issue Jan 6, 2025 · 10 comments

Comments

@asturur
Copy link

asturur commented Jan 6, 2025

Hello everyone, i m working on a command line utility with optional UI.

I migrated a setup that included the c library curses to tview that uses tcell.
After finishing the migration i noticed that the application was considerably slower at starting up:

So i digged into what it could be and i made this test program:

package main

import (
    "github.com/gdamore/tcell/v2"
)

func main() {
    print("hello slow world")
    print(tcell.Color100)
}

this software compiled to run on a linux32 env with a slow 800mhz arm core

/media/fat/Scripts# time zaparoo.sh
hello slow world
real	0m0.576s
user	0m0.549s
sys	0m0.022s

while if i remove the tcell import i get the following:

/media/fat/Scripts# time zaparoo.sh
hello slow world
real	0m0.010s
user	0m0.006s
sys	0m0.006s

To be clear the start up times when i m running the real code of the service is around 0.014 so very similar to an empty program run.

Just importing tcell without displaying anything creates a decent delay.

So i wanted to ask if this is necessary and if it can be patched maybe adding an optional lazy initialization that i can run whenever i need to start using tcell actually rather than just including in my binary.

Thank you

@kivattt
Copy link
Contributor

kivattt commented Jan 9, 2025

I suspect this is because of the (lowercase!) init() functions in tcell, which are ran before your main function can run

Notably,
files under terminfo/ calling AddTerminfo(), locking and unlocking a mutex, and encoding/all.go calling RegisterEncoding(), locking and unlocking a mutex.

I haven't tried this and I'm just speculating here, but you could try forking tcell and changing the init() functions in the places mentioned above to call your own version of AddTerminfo() and RegisterEncoding() respectively, where you don't lock and unlock a mutex.
I think this should be fine, since the Go language specification says that init() functions are called in order, but I could be wrong on that.

This article explains that all the init() functions in a package are ran when you import them:
https://www.digitalocean.com/community/tutorials/understanding-init-in-go

On my laptop, the difference is much less dramatic, with your code snippet taking 26 milliseconds to run, and not importing tcell taking 2 milliseconds to run (median, measured 10 times with time).

go version go1.22.2 linux/amd64
CPU: 11th Gen Intel i5-1135G7 (8) @ 4.200GHz

@kivattt
Copy link
Contributor

kivattt commented Jan 9, 2025

I just tried removing the mutex locks, and it made no difference so disregard that...
The issue might be in some other package that tcell depends on

@gdamore
Copy link
Owner

gdamore commented Jan 9, 2025

Uncontended locks are cheap, ... really cheap. I would not have expected that to be a problem.

I think it's more likely this one:

func init() {
	// The defaults for the runewidth package are poorly chosen for terminal
	// applications.  We however will honor the setting in the environment if
	// it is set.
	if os.Getenv("RUNEWIDTH_EASTASIAN") == "" {
		runewidth.DefaultCondition.EastAsianWidth = false
	}

	// For performance reasons, we create a lookup table.  However, some users
	// might be more memory conscious.  If that's you, set the TCELL_MINIMIZE
	// environment variable.
	if os.Getenv("TCELL_MINIMIZE") == "" {
		runewidth.CreateLUT()
	}
}

TCELL_MINIMIZE will save the effort to build the look up table. That could probably have been deferred and done lazily instead of during initi(), as its pretty expensive to build it, but it makes a noticeable difference for heavy users of Unicode.

@asturur
Copy link
Author

asturur commented Jan 9, 2025

I m mostly sure is some init function. On my side i tried the same program adding all the 4 deps of tcell and the script was fast.

I still do not know how to use a profiler in go and i m not able to pinpoint the exact line

@asturur
Copy link
Author

asturur commented Jan 9, 2025

Uncontended locks are cheap, ... really cheap. I would not have expected that to be a problem.

I think it's more likely this one:

func init() {
	// The defaults for the runewidth package are poorly chosen for terminal
	// applications.  We however will honor the setting in the environment if
	// it is set.
	if os.Getenv("RUNEWIDTH_EASTASIAN") == "" {
		runewidth.DefaultCondition.EastAsianWidth = false
	}

	// For performance reasons, we create a lookup table.  However, some users
	// might be more memory conscious.  If that's you, set the TCELL_MINIMIZE
	// environment variable.
	if os.Getenv("TCELL_MINIMIZE") == "" {
		runewidth.CreateLUT()
	}
}

TCELL_MINIMIZE will save the effort to build the look up table. That could probably have been deferred and done lazily instead of during initi(), as its pretty expensive to build it, but it makes a noticeable difference for heavy users of Unicode.

my first gut feeling was the init of runewidth with creatlut but it isn't, i tried it, it adds like 4ms on my slow machine. I will try again.
My understanding is that runewdith creates the lut in its init function anyway.

I ll make a test again thank you for the info

@gdamore
Copy link
Owner

gdamore commented Jan 9, 2025

Confirmed... its that look up table creation that is hurting.

What I think I can do, as I said, is defer that to a start up. I could also defer it to a go routine as well.

@gdamore
Copy link
Owner

gdamore commented Jan 9, 2025

Its easy to test... simply time with "TCELL_MINIMIZE=1" vs not having it set.

@gdamore
Copy link
Owner

gdamore commented Jan 9, 2025

So its really a concern not just for emoji, but for cases where the user has characters that fall outside of a narrow range of ASCII. Thats where it can make a big difference. Ideally we would like that LUT to be pre-compiled, and just have our binaries be a MB bigger if needed. I might see if the owner of that repo is amenable.

@gdamore
Copy link
Owner

gdamore commented Jan 9, 2025

Actually, I'm not really seeing a performance benefit from calling the createLUT at all. Stay tuned, I think I'm just going to remove that call.

@gdamore gdamore changed the title Adding tcell as a dependency adds a considerable amount of startup time to the application Adding tcell as a dependency delays startup time Jan 9, 2025
gdamore added a commit that referenced this issue Jan 9, 2025
We no longer bother to create the lookup table for runewidth.
I had expected this to make a significant difference, but I'm
finding that it does not, but it does make the startup time even
for applications that only import and never create a screen,
considerably worse.  Dozens of milliseconds, even up to half a
second.

Applications that need this (persumably some heavy Emoji or
EastAsian users) can solve for themselves by importing the runewidth
package (same version) and calling the CreateLUT method.
@gdamore gdamore closed this as completed in 522d1e6 Jan 9, 2025
@asturur
Copy link
Author

asturur commented Jan 9, 2025

Thank you that was fast.
Now it think that playing with go.mod file i can force tview to use this updated dep or at worst i ll ask them to update.

Thank you again

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

3 participants