From 09905fbd0741da204d405c128eef3e82e0aa4d8d Mon Sep 17 00:00:00 2001 From: Keith Winstein Date: Wed, 14 Dec 2011 03:56:28 -0500 Subject: [PATCH] Better speculative local echo/editing. Also outputs debugging/timing info --- networktransport.hpp | 3 + parser.cpp | 5 - stm.cpp | 2 +- stmclient.cpp | 21 +- terminaldisplay.cpp | 4 +- terminalframebuffer.cpp | 16 + terminalframebuffer.hpp | 10 +- terminalfunctions.cpp | 2 +- terminaloverlay.cpp | 712 +++++++++++++++++++++++----------------- terminaloverlay.hpp | 178 +++++----- transportsender.hpp | 3 + 11 files changed, 556 insertions(+), 400 deletions(-) diff --git a/networktransport.hpp b/networktransport.hpp index a502fea..35f7d71 100644 --- a/networktransport.hpp +++ b/networktransport.hpp @@ -79,6 +79,9 @@ namespace Network { void set_verbose( void ) { sender.set_verbose(); verbose = true; } void set_send_delay( int new_delay ) { sender.set_send_delay( new_delay ); } + + uint64_t get_sent_state_acked( void ) { return sender.get_sent_state_acked(); } + uint64_t get_sent_state_last( void ) { return sender.get_sent_state_last(); } }; } diff --git a/parser.cpp b/parser.cpp index f1b176e..ca53486 100644 --- a/parser.cpp +++ b/parser.cpp @@ -39,11 +39,6 @@ std::list Parser::Parser::input( wchar_t ch ) Parser::UTF8Parser::UTF8Parser() : parser(), buf_len( 0 ) { - if ( strcmp( nl_langinfo( CODESET ), "UTF-8" ) != 0 ) { - fprintf( stderr, "stm requires a UTF-8 locale.\n" ); - throw std::string( "stm requires a UTF-8 locale." ); - } - assert( BUF_SIZE >= MB_CUR_MAX ); } diff --git a/stm.cpp b/stm.cpp index 758f726..4c6e38c 100644 --- a/stm.cpp +++ b/stm.cpp @@ -28,7 +28,7 @@ int main( int argc, char *argv[] ) client.main(); client.shutdown(); - printf( "\n[stm is exiting.]\n" ); + printf( "\n[mosh is exiting.]\n" ); return 0; } diff --git a/stmclient.cpp b/stmclient.cpp index 680a5b3..d534773 100644 --- a/stmclient.cpp +++ b/stmclient.cpp @@ -24,7 +24,7 @@ void STMClient::init( void ) { /* Verify locale calls for UTF-8 */ if ( strcmp( nl_langinfo( CODESET ), "UTF-8" ) != 0 ) { - fprintf( stderr, "stm requires a UTF-8 locale.\n" ); + fprintf( stderr, "mosh requires a UTF-8 locale.\n" ); exit( 1 ); } @@ -50,10 +50,19 @@ void STMClient::init( void ) /* Put terminal in application-cursor-key mode */ swrite( STDOUT_FILENO, Terminal::Emulator::open().c_str() ); + + /* Add our name to window title */ + overlays.set_title_prefix( wstring( L"[mosh] " ) ); } void STMClient::shutdown( void ) { + /* Restore screen state */ + overlays.get_notification_engine().set_notification_string( wstring( L"" ) ); + overlays.get_notification_engine().server_heard( timestamp() ); + overlays.set_title_prefix( wstring( L"" ) ); + output_new_frame(); + /* Restore terminal and terminal-driver state */ swrite( STDOUT_FILENO, Terminal::Emulator::close().c_str() ); @@ -126,7 +135,6 @@ void STMClient::output_new_frame( void ) Terminal::Framebuffer new_state( network->get_latest_remote_state().state.get_fb() ); /* apply local overlays */ - overlays.get_notification_engine().render_notification(); overlays.apply( new_state ); /* calculate minimal difference from where we are */ @@ -143,7 +151,9 @@ bool STMClient::process_network_input( void ) { network->recv(); - overlays.get_notification_engine().server_ping( network->get_latest_remote_state().timestamp ); + overlays.get_notification_engine().server_heard( network->get_latest_remote_state().timestamp ); + + overlays.get_prediction_engine().set_local_frame_acked( network->get_sent_state_acked() ); return true; } @@ -163,6 +173,8 @@ bool STMClient::process_user_input( int fd ) } if ( !network->shutdown_in_progress() ) { + overlays.get_prediction_engine().set_local_frame_sent( network->get_sent_state_last() ); + for ( int i = 0; i < bytes_read; i++ ) { char the_byte = buf[ i ]; @@ -232,6 +244,9 @@ bool STMClient::process_resize( void ) i->state.act( &res ); } + /* tell prediction engine */ + overlays.get_prediction_engine().reset(); + return true; } diff --git a/terminaldisplay.cpp b/terminaldisplay.cpp index a18e4ab..18abe0c 100644 --- a/terminaldisplay.cpp +++ b/terminaldisplay.cpp @@ -17,8 +17,8 @@ std::string Display::new_frame( bool initialized, const Framebuffer &last, const || (f.get_window_title() != frame.last_frame.get_window_title()) ) { /* set window title */ frame.append( "\033]0;" ); - std::vector window_title = f.get_window_title(); - for ( std::vector::iterator i = window_title.begin(); + const std::deque &window_title( f.get_window_title() ); + for ( auto i = window_title.begin(); i != window_title.end(); i++ ) { snprintf( tmp, 64, "%lc", *i ); diff --git a/terminalframebuffer.cpp b/terminalframebuffer.cpp index f67b380..8500bd8 100644 --- a/terminalframebuffer.cpp +++ b/terminalframebuffer.cpp @@ -406,3 +406,19 @@ void Row::reset( int background_color ) i->reset( background_color ); } } + +void Framebuffer::prefix_window_title( const std::deque &s ) +{ + for ( auto i = s.rbegin(); i != s.rend(); i++ ) { + window_title.push_front( *i ); + } +} + +wchar_t Cell::debug_contents( void ) const +{ + if ( contents.empty() ) { + return '_'; + } else { + return contents.front(); + } +} diff --git a/terminalframebuffer.hpp b/terminalframebuffer.hpp index 2ce7d0e..9b2afb2 100644 --- a/terminalframebuffer.hpp +++ b/terminalframebuffer.hpp @@ -61,6 +61,8 @@ namespace Terminal { && (width == x.width) && (renditions == x.renditions) ); } + + wchar_t debug_contents( void ) const; }; class Row { @@ -179,7 +181,7 @@ namespace Terminal { class Framebuffer { private: std::deque rows; - std::vector window_title; + std::deque window_title; Row newrow( void ) { return Row( ds.get_width(), ds.get_background_rendition() ); } @@ -243,8 +245,10 @@ namespace Terminal { void reset( void ); void soft_reset( void ); - void set_window_title( std::vector s ) { window_title = s; } - std::vector get_window_title( void ) const { return window_title; } + void set_window_title( const std::deque &s ) { window_title = s; } + const std::deque & get_window_title( void ) const { return window_title; } + + void prefix_window_title( const std::deque &s ); void resize( int s_width, int s_height ); diff --git a/terminalfunctions.cpp b/terminalfunctions.cpp index dd7fcfc..1128ef5 100644 --- a/terminalfunctions.cpp +++ b/terminalfunctions.cpp @@ -466,7 +466,7 @@ void Dispatcher::OSC_dispatch( const Parser::OSC_End *act, Framebuffer *fb ) if ( OSC_string.size() >= 2 ) { if ( (OSC_string[ 0 ] == L'0') && (OSC_string[ 1 ] == L';') ) { - std::vector newtitle = OSC_string; + std::deque newtitle( OSC_string.begin(), OSC_string.end() ); newtitle.erase( newtitle.begin() ); newtitle.erase( newtitle.begin() ); fb->set_window_title( newtitle ); diff --git a/terminaloverlay.cpp b/terminaloverlay.cpp index 17408b6..3c56bd8 100644 --- a/terminaloverlay.cpp +++ b/terminaloverlay.cpp @@ -8,28 +8,41 @@ using namespace Overlay; -Validity OverlayElement::get_validity( const Framebuffer & ) const +void ConditionalOverlayCell::apply( Framebuffer &fb, bool show_tentative, int row, bool flag ) const { - return (timestamp() < expiration_time) ? Pending : IncorrectOrExpired; -} - -void OverlayCell::apply( Framebuffer &fb ) const -{ - if ( (row >= fb.ds.get_height()) + if ( (!active) + || (row >= fb.ds.get_height()) || (col >= fb.ds.get_width()) ) { return; } - if ( !(*(fb.get_mutable_cell( row, col )) == replacement) ) { + if ( tentative && (!show_tentative) ) { + return; + } + + /* + fprintf( stderr, "APPLYING char %lc to (%d, %d)\n", + replacement.debug_contents(), row, col ); + */ + + if ( !(*(fb.get_cell( row, col )) == replacement) ) { *(fb.get_mutable_cell( row, col )) = replacement; + uint64_t now = timestamp(); + if ( display_time >= now ) { + display_time = now; + } if ( flag ) { fb.get_mutable_cell( row, col )->renditions.underlined = true; } } } -Validity ConditionalOverlayCell::get_validity( const Framebuffer &fb ) const +Validity ConditionalOverlayCell::get_validity( const Framebuffer &fb, int row, uint64_t current_frame ) const { + if ( !active ) { + return Inactive; + } + if ( (row >= fb.ds.get_height()) || (col >= fb.ds.get_width()) ) { return IncorrectOrExpired; @@ -37,10 +50,16 @@ Validity ConditionalOverlayCell::get_validity( const Framebuffer &fb ) const const Cell ¤t = *( fb.get_cell( row, col ) ); - if ( (timestamp() < expiration_time) && (current == original_contents) ) { + /* see if it hasn't been updated yet */ + if ( (current_frame < expiration_frame) ) { return Pending; } + /* special case deletion */ + if ( current.contents.empty() && (replacement.contents.size() == 1) && (replacement.contents.front() == 0x20) ) { + return Correct; + } + if ( current == replacement ) { return Correct; } else { @@ -48,224 +67,76 @@ Validity ConditionalOverlayCell::get_validity( const Framebuffer &fb ) const } } -void CursorMove::apply( Framebuffer &fb ) const +Validity ConditionalCursorMove::get_validity( const Framebuffer &fb, uint64_t current_frame ) const { - assert( new_row < fb.ds.get_height() ); - assert( new_col < fb.ds.get_width() ); + if ( !active ) { + return Inactive; + } + + if ( (row >= fb.ds.get_height()) + || (col >= fb.ds.get_width()) ) { + fprintf( stderr, "Crazy cursor (%d,%d)!\n", row, col ); + return IncorrectOrExpired; + } + + if ( (fb.ds.get_cursor_col() == col) + && (fb.ds.get_cursor_row() == row) ) { + return Correct; + } else if ( current_frame < expiration_frame ) { + return Pending; + } else { + + fprintf( stderr, "Bad cursor in %d (i thought %d vs %d).\n", (int)current_frame, + col, fb.ds.get_cursor_col() ); + return IncorrectOrExpired; + } +} + +void ConditionalCursorMove::apply( Framebuffer &fb ) const +{ + if ( !active ) { + return; + } + + int target_row = row; + int target_col = col; + + if ( show_frozen_cursor ) { + target_row = frozen_row; + target_col = frozen_col; + } + + assert( target_row < fb.ds.get_height() ); + assert( target_col < fb.ds.get_width() ); assert( !fb.ds.origin_mode ); - fb.ds.move_row( new_row, false ); - fb.ds.move_col( new_col, false, false ); + fb.ds.move_row( target_row, false ); + fb.ds.move_col( target_col, false, false ); } -Validity ConditionalCursorMove::get_validity( const Framebuffer &fb ) const -{ - if ( (new_row >= fb.ds.get_height()) - || (new_col >= fb.ds.get_width()) ) { - return IncorrectOrExpired; - } - - if ( timestamp() < expiration_time ) { - return Pending; - } - - if ( (fb.ds.get_cursor_col() == new_col) - && (fb.ds.get_cursor_row() == new_row) ) { - return Correct; - } else { - return IncorrectOrExpired; - } -} - -void OverlayEngine::clear( void ) -{ - for_each( elements.begin(), elements.end(), []( OverlayElement *x ){ delete x; } ); - elements.clear(); -} - -OverlayEngine::~OverlayEngine() -{ - clear(); -} - -void OverlayEngine::apply( Framebuffer &fb ) const -{ - for_each( elements.begin(), elements.end(), - [&fb]( OverlayElement *x ) { x->apply( fb ); } ); -} - -void PredictionEngine::cull( const Framebuffer &fb ) -{ - uint64_t now = timestamp(); - - auto i = elements.begin(); - while ( i != elements.end() ) { - /* update echo timeout state */ - if ( (typeid( ConditionalOverlayCell ) == typeid( **i )) - && ((*i)->get_validity( fb ) == Correct) ) { - double R = now - (*i)->prediction_time; - if ( !RTT_hit ) { /* first measurement */ - SRTT = R; - RTTVAR = R / 2; - RTT_hit = true; - } else { - const double alpha = 1.0 / 32.0; - const double beta = 1.0 / 16.0; - - RTTVAR = (1 - beta) * RTTVAR + ( beta * fabs( SRTT - R ) ); - SRTT = (1 - alpha) * SRTT + ( alpha * R ); - } - } - - /* eliminate predictions proven correct or incorrect */ - if ( (*i)->get_validity( fb ) != Pending ) { - delete (*i); - i = elements.erase( i ); - } else { - i++; - } - } - - if ( SRTT > 150 ) flagging = true; /* start underlining predicted chars */ - if ( SRTT + 4 * RTTVAR < 100 ) flagging = false; /* use some hysterisis to avoid flapping */ -} - -OverlayCell::OverlayCell( uint64_t expiration_time, int s_row, int s_col, int background_color ) - : OverlayElement( expiration_time ), row( s_row ), col( s_col ), replacement( background_color ) -{} - -CursorMove::CursorMove( uint64_t expiration_time, int s_new_row, int s_new_col ) - : OverlayElement( expiration_time ), new_row( s_new_row ), new_col( s_new_col ) -{} - NotificationEngine::NotificationEngine() - : needs_render( true ), - last_word( timestamp() ), - last_render( 0 ), + : last_word_from_server( timestamp() ), message(), - message_expiration( 0 ) + message_expiration( -1 ) {} -void NotificationEngine::server_ping( uint64_t s_last_word ) -{ - if ( s_last_word - last_word > 4000 ) { - needs_render = true; - } - - last_word = s_last_word; -} - -void NotificationEngine::set_notification_string( const wstring s_message ) -{ - message = s_message; - message_expiration = timestamp() + 1100; - needs_render = true; -} - -void NotificationEngine::render_notification( void ) -{ - uint64_t now = timestamp(); - - if ( (now - last_render < 250) && (!needs_render) ) { - return; - } - - needs_render = false; - last_render = now; - - clear(); - - /* determine string to draw */ - if ( now >= message_expiration ) { - message.clear(); - } - - bool time_expired = now - last_word > 5000; - - wchar_t tmp[ 128 ]; - - if ( message.empty() && (!time_expired) ) { - return; - } else if ( message.empty() && time_expired ) { - swprintf( tmp, 128, L"[stm] Last contact %.0f seconds ago. [To quit: Ctrl-^ .]", (double)(now - last_word) / 1000.0 ); - } else if ( (!message.empty()) && (!time_expired) ) { - swprintf( tmp, 128, L"[stm] %ls [To quit: Ctrl-^ .]", message.c_str() ); - } else { - swprintf( tmp, 128, L"[stm] %ls (%.0f s without contact.) [To quit: Ctrl-^ .]", message.c_str(), - (double)(now - last_word) / 1000.0 ); - } - - wstring string_to_draw( tmp ); - - int overlay_col = 0; - bool dirty = false; - OverlayCell template_cell( now + 1100, 0 /* row */, -1 /* col */, 0 /* background_color */ ); - - template_cell.replacement.renditions.bold = true; - template_cell.replacement.renditions.foreground_color = 37; - template_cell.replacement.renditions.background_color = 44; - - OverlayCell current( template_cell ); - - for ( wstring::const_iterator i = string_to_draw.begin(); i != string_to_draw.end(); i++ ) { - wchar_t ch = *i; - int chwidth = ch == L'\0' ? -1 : wcwidth( ch ); - - switch ( chwidth ) { - case 1: /* normal character */ - case 2: /* wide character */ - /* finish current cell */ - if ( dirty ) { - elements.push_back( new OverlayCell( current ) ); - dirty = false; - } - - /* initialize new cell */ - current = template_cell; - current.col = overlay_col; - current.replacement.contents.push_back( ch ); - current.replacement.width = chwidth; - overlay_col += chwidth; - dirty = true; - break; - - case 0: /* combining character */ - if ( current.replacement.contents.empty() ) { - /* string starts with combining character?? */ - /* emulate fallback rendering */ - current = template_cell; - current.col = overlay_col; - current.replacement.contents.push_back( 0xA0 ); /* no-break space */ - current.replacement.width = 1; - overlay_col++; - dirty = true; - } - - current.replacement.contents.push_back( ch ); - break; - - case -1: - break; - - default: - assert( false ); - } - } - - if ( dirty ) { - elements.push_back( new OverlayCell( current ) ); - } -} - void NotificationEngine::apply( Framebuffer &fb ) const { - if ( elements.empty() ) { + uint64_t now = timestamp(); + + bool time_expired = need_countup( now ); + + if ( message.empty() && !time_expired ) { return; } assert( fb.ds.get_width() > 0 ); assert( fb.ds.get_height() > 0 ); + /* hide cursor if necessary */ + if ( fb.ds.get_cursor_row() == 0 ) { + fb.ds.cursor_visible = false; + } /* draw bar across top of screen */ Cell notification_bar( 0 ); @@ -277,135 +148,356 @@ void NotificationEngine::apply( Framebuffer &fb ) const *(fb.get_mutable_cell( 0, i )) = notification_bar; } - /* hide cursor if necessary */ - if ( fb.ds.get_cursor_row() == 0 ) { - fb.ds.cursor_visible = false; + /* write message */ + wchar_t tmp[ 128 ]; + + if ( message.empty() && (!time_expired) ) { + return; + } else if ( message.empty() && time_expired ) { + swprintf( tmp, 128, L"mosh: Last contact %.0f seconds ago. [To quit: Ctrl-^ .]", (double)(now - last_word_from_server) / 1000.0 ); + } else if ( (!message.empty()) && (!time_expired) ) { + swprintf( tmp, 128, L"mosh: %ls [To quit: Ctrl-^ .]", message.c_str() ); + } else { + swprintf( tmp, 128, L"mosh: %ls (%.0f s without contact.) [To quit: Ctrl-^ .]", message.c_str(), + (double)(now - last_word_from_server) / 1000.0 ); } - OverlayEngine::apply( fb ); + wstring string_to_draw( tmp ); + + int overlay_col = 0; + + Cell *combining_cell = fb.get_mutable_cell( 0, 0 ); + + /* We unfortunately duplicate the terminal's logic for how to render a Unicode sequence into graphemes */ + for ( wstring::const_iterator i = string_to_draw.begin(); i != string_to_draw.end(); i++ ) { + if ( overlay_col >= fb.ds.get_width() ) { + break; + } + + wchar_t ch = *i; + int chwidth = ch == L'\0' ? -1 : wcwidth( ch ); + Cell *this_cell = nullptr; + + switch ( chwidth ) { + case 1: /* normal character */ + case 2: /* wide character */ + this_cell = fb.get_mutable_cell( 0, overlay_col ); + fb.reset_cell( this_cell ); + this_cell->renditions.bold = true; + this_cell->renditions.foreground_color = 37; + this_cell->renditions.background_color = 44; + + this_cell->contents.push_back( ch ); + this_cell->width = chwidth; + combining_cell = this_cell; + + overlay_col += chwidth; + break; + case 0: /* combining character */ + if ( !combining_cell ) { + break; + } + + if ( combining_cell->contents.size() == 0 ) { + assert( combining_cell->width == 1 ); + combining_cell->fallback = true; + overlay_col++; + } + + if ( combining_cell->contents.size() < 16 ) { + combining_cell->contents.push_back( ch ); + } + break; + case -1: /* unprintable character */ + break; + default: + assert( false ); + } + } +} + +void NotificationEngine::adjust_message( void ) +{ + if ( timestamp() >= message_expiration ) { + message.clear(); + } } void OverlayManager::apply( Framebuffer &fb ) { - predictions.calculate_score( fb ); - - /* eliminate predictions proven correct or incorrect and update echo timers */ predictions.cull( fb ); - - if ( predictions.get_score() > 3 ) { - predictions.apply( fb ); - } - + predictions.apply( fb ); + notifications.adjust_message(); notifications.apply( fb ); + title.apply( fb ); } -void PredictionEngine::calculate_score( const Framebuffer &fb ) +int OverlayManager::wait_time( void ) { - for ( auto i = begin(); i != end(); i++ ) { - switch( (*i)->get_validity( fb ) ) { - case Pending: - break; - case Correct: - score++; - break; - case IncorrectOrExpired: - score = 0; - clear(); - return; + uint64_t next_expiry = INT_MAX; + + uint64_t message_delay = notifications.get_message_expiration() - timestamp(); + + if ( message_delay < next_expiry ) { + next_expiry = message_delay; + } + + if ( notifications.need_countup( timestamp() ) && ( next_expiry > 1000 ) ) { + next_expiry = 1000; + } + + return next_expiry; +} + +void TitleEngine::set_prefix( const wstring s ) +{ + prefix = deque( s.begin(), s.end() ); +} + +void ConditionalOverlayRow::apply( Framebuffer &fb, bool show_tentative, bool flag ) const +{ + for_each( overlay_cells.begin(), overlay_cells.end(), [&]( const ConditionalOverlayCell &x ) { x.apply( fb, show_tentative, row_num, flag ); } ); +} + +void PredictionEngine::apply( Framebuffer &fb ) const +{ + if ( (score > 0) || cursor.show_frozen_cursor ) { + cursor.apply( fb ); + } + for_each( overlays.begin(), overlays.end(), [&]( const ConditionalOverlayRow &x ){ x.apply( fb, score > 0, flagging ); } ); +} + +void PredictionEngine::reset( void ) +{ + overlays.clear(); + cursor.reset(); + become_tentative(); +} + +void PredictionEngine::cull( const Framebuffer &fb ) +{ + if ( score > 0 ) { + cursor.thaw(); + } + + uint64_t now = timestamp(); + + /* don't increment score just for correct cursor position */ + switch ( cursor.get_validity( fb, local_frame_acked) ) { + case IncorrectOrExpired: + cursor.reset(); + become_tentative(); + return; + break; + default: + break; + } + + uint64_t max_delay = 0; + + auto i = overlays.begin(); + while ( i != overlays.end() ) { + auto inext = i; + inext++; + if ( (i->row_num < 0) || (i->row_num >= fb.ds.get_height()) ) { + overlays.erase( i ); + i = inext; + continue; } + + for ( auto j = i->overlay_cells.begin(); j != i->overlay_cells.end(); j++ ) { + switch ( j->get_validity( fb, i->row_num, local_frame_acked ) ) { + case IncorrectOrExpired: + if ( j->tentative ) { + fprintf( stderr, "Bad tentative prediction in row %d, col %d (thought %lc, was %lc)\n", + i->row_num, j->col, + j->replacement.debug_contents(), fb.get_cell( i->row_num, j->col )->debug_contents() ); + j->reset(); + become_tentative(); + if ( j->display_time != uint64_t(-1) ) { + fprintf( stderr, "TIMING %ld - %ld (TENT)\n", time(NULL), now - j->display_time ); + } + } else { + fprintf( stderr, "[%d=>%d] (score=%d) Killing prediction in row %d, col %d (thought %lc, was %lc)\n", + (int)local_frame_acked, (int)j->expiration_frame, + score, + i->row_num, j->col, + j->replacement.debug_contents(), fb.get_cell( i->row_num, j->col )->debug_contents() ); + reset(); + if ( j->display_time != uint64_t(-1) ) { + fprintf( stderr, "TIMING %ld - %ld\n", time(NULL), now - j->display_time ); + } + return; + } + break; + case Correct: + if ( j->display_time != uint64_t(-1) ) { + fprintf( stderr, "TIMING %ld + %ld\n", now, now - j->display_time ); + } + + j->reset(); + if ( j->prediction_time > prediction_checkpoint ) { + score++; + } + break; + case Pending: + max_delay = max( max_delay, now - j->prediction_time ); + break; + default: + break; + } + } + + i = inext; + } + + if ( max_delay > 100 ) { + flagging = true; + } else if ( max_delay < 50 ) { + flagging = false; + } +} + +ConditionalOverlayRow & PredictionEngine::get_or_make_row( int row_num, int num_cols ) +{ + auto it = find_if( overlays.begin(), overlays.end(), + [&]( const ConditionalOverlayRow &x ) { return x.row_num == row_num; } ); + + if ( it != overlays.end() ) { + return *it; + } else { + /* make row */ + ConditionalOverlayRow r( row_num ); + r.overlay_cells.reserve( num_cols ); + for ( int i = 0; i < num_cols; i++ ) { + r.overlay_cells.push_back( ConditionalOverlayCell( i ) ); + assert( r.overlay_cells[ i ].col == i ); + } + overlays.push_back( r ); + return overlays.back(); } } void PredictionEngine::new_user_byte( char the_byte, const Framebuffer &fb ) { + list actions( parser.input( the_byte ) ); + uint64_t now = timestamp(); - if ( elements.empty() ) { - /* starting from scratch */ - - elements.push_front( new ConditionalCursorMove( now + prediction_len(), - fb.ds.get_cursor_row(), - fb.ds.get_cursor_col() ) ); - } + for ( auto it = actions.begin(); it != actions.end(); it++ ) { + Parser::Action *act = *it; - assert( typeid( ConditionalCursorMove ) == typeid( *elements.front() ) ); - ConditionalCursorMove *ccm = static_cast( elements.front() ); + if ( typeid( *act ) == typeid( Parser::Print ) ) { + /* make new prediction */ - if ( (ccm->new_row >= fb.ds.get_height()) || (ccm->new_col >= fb.ds.get_width()) ) { - return; - } + wchar_t ch = act->ch; + /* XXX handle wide characters */ - if ( (the_byte >= 0x20) && (the_byte <= 0x7E) && (ccm->new_col < fb.ds.get_width() - 2) ) { - /* XXX need to kill existing prediction if present */ + if ( !cursor.active ) { + /* initialize new cursor prediction */ + cursor.row = fb.ds.get_cursor_row(); + cursor.col = fb.ds.get_cursor_col(); + cursor.active = true; + cursor.prediction_time = now; + cursor.expiration_frame = local_frame_sent + 1; + } + + if ( ch == 0x7f ) { /* backspace */ + // fprintf( stderr, "Backspace.\n" ); + if ( cursor.col > 0 ) { + cursor.col--; - const Cell *existing_cell = fb.get_cell( ccm->new_row, ccm->new_col ); + cursor.prediction_time = now; + cursor.expiration_frame = local_frame_sent + 1; - if ( (existing_cell->contents.size() == 1) - && (existing_cell->contents.front() == the_byte) ) { - /* do nothing */ - } else if ( (existing_cell->contents.empty() || ((existing_cell->contents.size() == 1) - && ( (existing_cell->contents.front() == 0x20) - || (existing_cell->contents.front() == 0xA0) ))) - && ( the_byte == 0x20 ) ) { - /* don't attempt to change existing blank or space cells if user has typed space */ - } else { - Renditions current_renditions = fb.ds.get_renditions(); + ConditionalOverlayCell &cell = get_or_make_row( cursor.row, fb.ds.get_width() ).overlay_cells[ cursor.col ]; + cell.active = true; + cell.tentative = (score <= 0); + cell.prediction_time = now; + cell.expiration_frame = local_frame_sent + 1; + cell.replacement.renditions = fb.ds.get_renditions(); + cell.replacement.contents.clear(); + cell.replacement.contents.push_back( 0x20 ); + cell.display_time = -1; + } + } else if ( (ch < 0x20) || (ch > 0x7E) ) { + /* unknown print */ + become_tentative(); + } else { + /* don't attempt to change existing blank or space cells if user has typed space */ + const Cell *existing_cell = fb.get_cell( cursor.row, cursor.col ); + if ( ( existing_cell->contents.empty() || ((existing_cell->contents.size() == 1) + && ( (existing_cell->contents.front() == 0x20) + || (existing_cell->contents.front() == 0xA0) ))) + && ( ch == 0x20 ) ) { + /* do nothing */ + } else { + assert( cursor.row >= 0 ); + assert( cursor.col >= 0 ); + assert( cursor.row < fb.ds.get_height() ); + assert( cursor.col < fb.ds.get_width() ); - ConditionalOverlayCell *coc = new ConditionalOverlayCell( now + prediction_len(), - ccm->new_row, ccm->new_col, - current_renditions.background_color, - *existing_cell ); - - coc->replacement = *existing_cell; - coc->replacement.renditions = current_renditions; - coc->replacement.contents.clear(); - coc->replacement.contents.push_back( the_byte ); - coc->flag = flagging; + if ( cursor.col + 1 >= fb.ds.get_width() ) { + /* prediction in the last column is tricky */ + /* e.g., emacs will show wrap character, shell will just put the character there */ + become_tentative(); + cursor.freeze(); + } - elements.push_back( coc ); + ConditionalOverlayCell &cell = get_or_make_row( cursor.row, fb.ds.get_width() ).overlay_cells[ cursor.col ]; + cell.active = true; + cell.tentative = (score <= 0); + cell.prediction_time = now; + cell.expiration_frame = local_frame_sent + 1; + cell.replacement.renditions = fb.ds.get_renditions(); + cell.replacement.contents.clear(); + cell.replacement.contents.push_back( ch ); + cell.display_time = -1; + + /* + fprintf( stderr, "[%d=>%d] Predicting %lc in row %d, col %d\n", + (int)local_frame_acked, (int)cell.expiration_frame, + ch, cursor.row, cursor.col ); + */ + } + + cursor.prediction_time = now; + cursor.expiration_frame = local_frame_sent + 1; + cursor.col++; + + /* do we need to wrap? */ + if ( cursor.col >= fb.ds.get_width() ) { + become_tentative(); + cursor.col--; + cursor.freeze(); + cursor.col = 0; + if ( cursor.row == fb.ds.get_height() - 1 ) { + for ( auto i = overlays.begin(); i != overlays.end(); i++ ) { + i->row_num--; + for ( auto j = i->overlay_cells.begin(); j != i->overlay_cells.end(); j++ ) { + if ( j->active ) { + // j->tentative = (score <= 0); + j->prediction_time = now; + j->expiration_frame = local_frame_sent + 1; + } + } + } + } else { + cursor.row++; + } + } + } + } else if ( typeid( *act ) == typeid( Parser::Execute ) ) { + become_tentative(); + cursor.freeze(); } - ccm->new_col++; - ccm->expiration_time = now + prediction_len(); - } else { - clear(); - score = 0; + delete act; } } -int OverlayManager::wait_time( void ) +void PredictionEngine::become_tentative( void ) { - uint64_t now = timestamp(); - - uint64_t next_expiry = uint64_t( -1 ); - - for ( auto i = notifications.begin(); i != notifications.end(); i++ ) { - if ( (*i)->expiration_time < next_expiry ) { - next_expiry = (*i)->expiration_time; - } - } - - for ( auto i = predictions.begin(); i != predictions.end(); i++ ) { - if ( (*i)->expiration_time < next_expiry ) { - next_expiry = (*i)->expiration_time; - } - } - - int ret = next_expiry - now; - if ( ret < 0 ) { - return INT_MAX; - } else { - return ret; - } -} - -int PredictionEngine::prediction_len( void ) -{ - uint64_t RTO = lrint( ceil( 1.5 * SRTT + 8 * RTTVAR ) ); - if ( RTO < 20 ) { - RTO = 20; - } else if ( RTO > 4000 ) { - RTO = 4000; - } - return RTO; + prediction_checkpoint = timestamp(); + score = 0; } diff --git a/terminaloverlay.hpp b/terminaloverlay.hpp index 1ba5e7d..b684cb9 100644 --- a/terminaloverlay.hpp +++ b/terminaloverlay.hpp @@ -3,8 +3,9 @@ #include "terminalframebuffer.hpp" #include "network.hpp" +#include "parser.hpp" -#include +#include namespace Overlay { using namespace Terminal; @@ -14,118 +15,142 @@ namespace Overlay { enum Validity { Pending, Correct, - IncorrectOrExpired + IncorrectOrExpired, + Inactive }; - /* The individual elements of an overlay -- cursor movements and replaced cells */ - class OverlayElement { + class ConditionalOverlay { public: - uint64_t prediction_time, expiration_time; - bool flag; /* whether to bold for the user */ + uint64_t prediction_time, expiration_frame; + int col; + bool active; /* represents a prediction at all */ + bool tentative; /* whether to hide when score < 0 */ - virtual void apply( Framebuffer &fb ) const = 0; - virtual Validity get_validity( const Framebuffer & ) const; + ConditionalOverlay( uint64_t s_exp, int s_col ) + : prediction_time( timestamp() ), + expiration_frame( s_exp ), col( s_col ), + active( false ), + tentative( false ) + {} - OverlayElement( uint64_t s_expiration_time ) : prediction_time( timestamp() ), - expiration_time( s_expiration_time ), - flag( false ) {} - virtual ~OverlayElement() {} + virtual ~ConditionalOverlay() {} }; - class OverlayCell : public OverlayElement { + class ConditionalCursorMove : public ConditionalOverlay { + public: + int row; + + int frozen_col, frozen_row; /* after a control character */ + + bool show_frozen_cursor; + + void apply( Framebuffer &fb ) const; + + Validity get_validity( const Framebuffer &fb, uint64_t current_frame ) const; + + ConditionalCursorMove( uint64_t s_exp, int s_row, int s_col ) + : ConditionalOverlay( s_exp, s_col ), row( s_row ), frozen_col( -1 ), frozen_row( -1 ), + show_frozen_cursor( false ) + {} + + void freeze( void ) { if ( show_frozen_cursor ) { return; } frozen_col = col; frozen_row = row; show_frozen_cursor = true; } + void thaw( void ) { show_frozen_cursor = false; } + void reset( void ) { active = false; thaw(); } + }; + + class ConditionalOverlayCell : public ConditionalOverlay { public: - int row, col; Cell replacement; - OverlayCell( uint64_t expiration_time, int s_row, int s_col, int background_color ); - void apply( Framebuffer &fb ) const; - }; + mutable uint64_t display_time; - class ConditionalOverlayCell : public OverlayCell { - public: - Cell original_contents; + void apply( Framebuffer &fb, bool show_tentative, int row, bool flag ) const; + Validity get_validity( const Framebuffer &fb, int row, uint64_t current_frame ) const; - Validity get_validity( const Framebuffer &fb ) const; - - ConditionalOverlayCell( uint64_t expiration_time, int s_row, int s_col, int background_color, - Cell s_original_contents ) - : OverlayCell( expiration_time, s_row, s_col, background_color ), - original_contents( s_original_contents ) + ConditionalOverlayCell( int s_col ) + : ConditionalOverlay( 0, s_col ), + replacement( 0 ), + display_time( -1 ) {} + + void reset( void ) { active = false; display_time = -1; } }; - class CursorMove : public OverlayElement { + class ConditionalOverlayRow { public: - int new_row, new_col; + int row_num; + vector overlay_cells; - void apply( Framebuffer &fb ) const; + void apply( Framebuffer &fb, bool show_tentative, bool flag ) const; - CursorMove( uint64_t expiration_time, int s_new_row, int s_new_col ); + ConditionalOverlayRow( int s_row_num ) : row_num( s_row_num ), overlay_cells() {} }; - class ConditionalCursorMove : public CursorMove { - public: - Validity get_validity( const Framebuffer &fb ) const; - - ConditionalCursorMove( uint64_t expiration_time, int s_new_row, int s_new_col ) - : CursorMove( expiration_time, s_new_row, s_new_col ) - {} - }; - - /* the various overlays -- some predictive and some for local notifications */ - class OverlayEngine { - protected: - list elements; - - public: - virtual void apply( Framebuffer &fb ) const; - void clear( void ); - - typename list::const_iterator begin( void ) { return elements.begin(); } - typename list::const_iterator end( void ) { return elements.end(); } - - OverlayEngine() : elements() {} - virtual ~OverlayEngine(); - }; - - class NotificationEngine : public OverlayEngine { + /* the various overlays */ + class NotificationEngine { private: - bool needs_render; - - uint64_t last_word; - uint64_t last_render; - + uint64_t last_word_from_server; wstring message; uint64_t message_expiration; public: + bool need_countup( uint64_t ts ) const { return ts - last_word_from_server > 4500; } + void adjust_message( void ); void apply( Framebuffer &fb ) const; - void set_notification_string( const wstring s_message ); - const wstring &get_notification_string( void ) { return message; } - void server_ping( uint64_t s_last_word ); - void render_notification( void ); + void set_notification_string( const wstring s_message ) { message = s_message; message_expiration = timestamp() + 1000; } + const wstring &get_notification_string( void ) const { return message; } + void server_heard( uint64_t s_last_word ) { last_word_from_server = s_last_word; } + uint64_t get_message_expiration( void ) const { return message_expiration; } NotificationEngine(); }; - class PredictionEngine : public OverlayEngine { + class PredictionEngine { private: + Parser::UTF8Parser parser; + + list overlays; + + ConditionalCursorMove cursor; + int score; - /* use the TCP timeout algorithm to measure appropriate echo prediction timeout */ - bool RTT_hit; - double SRTT, RTTVAR; + uint64_t local_frame_sent, local_frame_acked; + + ConditionalOverlayRow & get_or_make_row( int row_num, int num_cols ); + + uint64_t prediction_checkpoint; + + void become_tentative( void ); + bool flagging; - int prediction_len( void ); public: - void cull( const Framebuffer &fb ); + void apply( Framebuffer &fb ) const; void new_user_byte( char the_byte, const Framebuffer &fb ); - void calculate_score( const Framebuffer &fb ); + void cull( const Framebuffer &fb ); - PredictionEngine() : score( 0 ), RTT_hit( false ), SRTT( 1000 ), RTTVAR( 500 ), flagging( false ) {} + void reset( void ); - int get_score( void ) { return score; } + void set_local_frame_sent( uint64_t x ) { local_frame_sent = x; } + void set_local_frame_acked( uint64_t x ) { local_frame_acked = x; } + + PredictionEngine( void ) : parser(), overlays(), cursor( 0, 0, 0 ), score( 0 ), + local_frame_sent( 0 ), local_frame_acked( 0 ), + prediction_checkpoint( timestamp() ), flagging( false ) + { + become_tentative(); + } + }; + + class TitleEngine { + private: + deque prefix; + + public: + void apply( Framebuffer &fb ) const { fb.prefix_window_title( prefix ); } + void set_prefix( const wstring s ); + TitleEngine() : prefix() {} }; /* the overlay manager */ @@ -133,6 +158,7 @@ namespace Overlay { private: NotificationEngine notifications; PredictionEngine predictions; + TitleEngine title; public: void apply( Framebuffer &fb ); @@ -140,7 +166,9 @@ namespace Overlay { NotificationEngine & get_notification_engine( void ) { return notifications; } PredictionEngine & get_prediction_engine( void ) { return predictions; } - OverlayManager() : notifications(), predictions() {} + void set_title_prefix( const wstring s ) { title.set_prefix( s ); } + + OverlayManager() : notifications(), predictions(), title() {} int wait_time( void ); }; diff --git a/transportsender.hpp b/transportsender.hpp index 0f790d1..b3ae541 100644 --- a/transportsender.hpp +++ b/transportsender.hpp @@ -94,6 +94,9 @@ namespace Network { bool get_shutdown_in_progress( void ) { return shutdown_in_progress; } bool get_shutdown_acknowledged( void ) { return sent_states.front().num == uint64_t(-1); } bool get_counterparty_shutdown_acknowledged( void ) { return fragmenter.last_ack_sent() == uint64_t(-1); } + uint64_t get_sent_state_acked( void ) { return sent_states.front().num; } + uint64_t get_sent_state_last( void ) { return sent_states.back().num; } + bool shutdown_ack_timed_out( void ); void set_send_delay( int new_delay ) { SEND_MINDELAY = new_delay; }