-
Notifications
You must be signed in to change notification settings - Fork 280
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
Performance Enhancement #171
Comments
So related to #167. I was thinking we can solve this by introducing a |
I agree that making a queue of "commands" and then translating to a string, or just interpreting them directly is an excellent solution. Would they all fit inside of a single enum? It is hard to say in what way this could all be implemented well, but I think it might be best to have a use crossterm::ScreenCommand::*;
screen().do(Goto(0,0));
screen().do(Say("Uses commands, which are compiled into the"));
screen().do(Goto(0,1));
screen().do(Say("equivalent ANSI string, or translated for WINAPI."));
let s = screen();
do!(s, Goto(2, 4), Color("Allows for a vec!-like macro", crossterm::Color::Magenta));
s.flush(); I feel like this simply adds to the usability, and allows for manual, or even "low-level" control over how things are printed. This is also assuming the original methods still work, even though they are inefficient: Edit: I notice now that |
Thanks for putting your idea in here. I was thinking about this as well. And had a similar idea with the commands. We need to try to make this a feature, which
trait TerminalCommand {
fn ansi_code() -> &'static str;
}
pub struct Goto(pub u16, pub u16);
impl TerminalCommnad for Goto {
fn ansi_code() -> &'static str {
return format!("{};{}H", self.0, self.1)
}
} That was my initial thought. Each command has an ANSI escape code representation. The user queues an implementation of this trait. On that implementation, we can call When we do the flush, there are two scenarios:
We can only improve the performance for Windows 10 and Unix systems because of those work with ANSI codes. And windows lower than 10 will be supported by this API, but won't notice any major performance difference. Where do we put this code? Since we use all kinds of commands, there are two options: 1) crossterm level, 2) own crate, which will depend on various other crossterm crates. |
I split my responses in two sections: Optimal WritingNo idea honestly, I'm somehow completely ignorant in this area. I think it would be great if people could optionally implement their own method of writing, though. A Separate CrateI'd say put the trait and the interpreter in its own crate, but not Commands implement a trait called, for example, trait ANSICommand {
fn ansi_code(&self) -> String;
#[cfg(windows)] // Not sure if these are possible in traits.
fn winapi_call(&self, hwnd: HWND) -> Result<(), SomeTypeOfError>;
} *Note: I wrote And the crate could come implemented with the default commands you wrote above, which could all be removed with that feature flag I can imagine several applications seeking an easy method for cross-platform ANSI terminal colors and styles, without relying on a full "terminal library", with mouse support, custom input, etc. |
I love this idea, I was thinking of matching the ANSI code at some WinApi translate layer, and then execute the WinApi command. But this work way better. Now we can let each crate provide a bunch of commands implementing this command trait. This trait can belong to So: crossterm_cursor Goto
UP
Down
Left
Right
SavePos
ResetPos
Hide
Show
Blink crossterm_terminal: Reset
Clear
SetSize crossterm_style:
With this idea, when the user only uses |
Great! I'm glad you like the trait idea. |
I can experiment with this idea and will have a branch soon. Or if you like to do this I am okay with it as well. But I want to make sure that we don't work both on the same code :). Also using the I do prefer another name for the |
You can check out #175 for a basic implementation for the cursor module. |
So I experimented a bit further, I propose to introduce two macro's that can be used to execute commands: This macro takes in command(s) and in case of ANSI codes, they will be written to the given writable type. They will be flushed if and only if the user flushes the buffer or the OS detects that the buffer is to full.
This macro takes in command(s) and executes that command directly.
How those macros will be used:
execute
Other #175 has a work in progress implementation (might not compile) |
I currently have a basic setup over here #175. Benchmark scenario: let mut stdout = ::std::io::stdout();
let instant1 = Instant::now();
for i in 0..10 {
for x in 0..200 {
for y in 0..50 {
schedule!(stdout, Goto(x, y), Hide, Output(y.to_string()));
}
}
}
println!("Elapsed with new command API {}", instant1.elapsed());
Elapsed with new command API 399.167104ms
The conclusion is that Linux seems to be way better in handling stdout stuff, and both platforms have a great performance improvement. The benchmark code can be found over in /crossteerm_cursor/examples/cursor.rs |
Possible alternative suggestion here? Seems like the issues are
To solve (1), it seems the conversation above is talking about a separate "refresh" / "flush" function for the user to call. For (2), I was thinking, instead of a clever (read: complex or unconventional) macro for a pipeline of actions, if we know that the issue mainly stems from positioning and printing to the screen, maybe we separate those two things? Have a "front" buffer that contains the most recent output and a "back" buffer that contains the diffs. When "flush" is called, the backend performs a diff and prints to the screen. Any operations for cursor and text is changing a background data struct so it shouldn't impact visual performance. For example,
then the resulting back buffer is: CursorX = 6 (col 7) if the terminal window was (5w x 3h): Either way, when the user "flushes" what is in the back buffer gets rendered. All operations therefore update the back buffer. In the case of a program loop, the user can enforce some kind of 60fps flush rate to keep a smooth visual response rate to a user's eyes. I believe this method is what 2d game engines also do to keep track of user input and what gets rendered on the screen. Also, with this idea of "back" buffers, you then can also implement multiple alternative screens as well. Thoughts? I mean, I like the macro idea of a command pipeline, but I rather not have custom implementations when I'm sure there will be more complexity down the road... |
I will get back to you soon, #175 is merged by the way, and you might be interested to try it and read the book/command.md. |
First) This sounds like an easy solution, but there's a big problem behind it. First of all, what is flush called for? As it was done, every command was written to 'stdout', and called here on flush. The problem with this is that some users don't want to write to the stdout. This problem can be solved by optimizing and controlling where commands are executed. The ideal situation would be if we let the user determine this. This is what termion does for example. All commands are written to a buffer specified by the user. This buffer is user-controlled and all termion does is write commands to this buffer. This is what crossterm had before. In crossterm it was possible to give your own TerminalOutput. Commands were written to it and the user was in control of this type. All of them had a problem with putting it in an Arc, and this made the API very difficult to use. second) In the API command, a You can check out this for more information. This API was just merged a short time before you made a comment. It is tested and implemented for TUI, cursive, and I think that it has everything that is currently missing |
What do you mean with custom types and more complexity? |
Going to close this, it is no longer relevant, if there are new concerns, it should be done in a new issue. |
Task Description
Crossterm flushes the buffer each time action with an ANSI-code is performed. This is not realistic in the case of moving the cursor a lot and meanwhile print characters. When calling those types of functions a lot, cross-term might behave a bit slow.
Task TODO
Example of what I mean:
winapi scenario of moving the cursor
ansi scenario to move the cursor
As you can see, in the case of ANSI codes we can store the codes and text of the user inside on string, however, with WinApi we don't store actions inside a string. But those are calls that need to be made to the console individually.
Somehow, we need to be able to store those WinApi commands in order ass well. So that if the user does a manual flush to the terminal, in the case of windows, we walk through all the WinApi commands queued and execute those, as well print the user input, done between those two cursor move events.
Notice
I do notice, Linux is way faster with console actions than windows is. I am not sure whether this is the problem of rust using winapi to write to the console window, winapi ANSI interpretation layer, crossterm performance issue. Windows is often forgotten in the Rust library, and would not be amazed if there is some performance issue there somewhere. But this needs to be proven of course.
The text was updated successfully, but these errors were encountered: