From 9fe88d1dd66e9e28f3e9f60c3baba495d7e534fc Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 19 Jul 2024 20:02:57 +0400 Subject: [PATCH 01/40] feat: Add CORS middleware to handle cross-origin requests --- tool/echo/echo.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tool/echo/echo.go b/tool/echo/echo.go index 636742c..ff6cee6 100644 --- a/tool/echo/echo.go +++ b/tool/echo/echo.go @@ -73,6 +73,23 @@ func authMiddleware(h http.Handler) http.Handler { }) } +// corsMiddleware is a middleware function that adds CORS headers to the response before passing it to the next handler. +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + // Handle preflight requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} + // main function initializes a new Centrifuge node and sets up event handlers for client connections, subscriptions, and RPCs. // It also sets up a websocket handler and a file server for serving static files. // The function waits for an exit signal before shutting down the node and exiting. @@ -309,6 +326,7 @@ func main() { websocketHandler := centrifuge.NewWebsocketHandler(node, centrifuge.WebsocketConfig{ ReadBufferSize: 1024, UseWriteBufferPool: true, + CheckOrigin: func(r *http.Request) bool { return true }, // Allow all connections. }) mux.Handle("/connection/websocket", authMiddleware(websocketHandler)) @@ -334,7 +352,7 @@ func main() { }) server := &http.Server{ - Handler: mux, + Handler: corsMiddleware(mux), Addr: ":" + strconv.Itoa(*port), ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, From 31a42a6ab7d8e6f4505e1379e8cb6853600a2171 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 19 Jul 2024 20:51:28 +0400 Subject: [PATCH 02/40] feat: Update launch.json and SpinifyInterface to improve WebSocket handling and add support for Protobuf transport --- .vscode/launch.json | 12 ++++++++---- lib/src/spinify_interface.dart | 2 +- lib/src/transport_ws_pb_js.dart | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c82468b..ff76443 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -132,8 +132,10 @@ "--reporter=expanded", "--platform=vm", // chrome "--file-reporter=json:coverage/tests.json", - "--timeout=30s", - "--concurrency=12" + "--timeout=5m", + "--concurrency=12", + "--chain-stack-traces", + /* "--name=Disconnect_temporarily" */ ], "args": [], /* "preLaunchTask": "echo:start", @@ -158,8 +160,10 @@ "--platform=chrome", "--compiler=dart2js", "--file-reporter=json:coverage/tests.json", - "--timeout=30s", - "--concurrency=12" + "--timeout=5m", + "--concurrency=12", + "--chain-stack-traces", + "--name=Disconnect_temporarily", ], "args": [], /* "preLaunchTask": "echo:start", diff --git a/lib/src/spinify_interface.dart b/lib/src/spinify_interface.dart index 8a4aa4a..dd6069b 100644 --- a/lib/src/spinify_interface.dart +++ b/lib/src/spinify_interface.dart @@ -155,7 +155,7 @@ abstract interface class ISpinifyHistoryOwner { /// Spinify remote procedure call interface. abstract interface class ISpinifyRemoteProcedureCall { /// Send arbitrary RPC and wait for response. - Future> rpc(String method, List data); + Future> rpc(String method, [List? data]); } /// Spinify metrics interface. diff --git a/lib/src/transport_ws_pb_js.dart b/lib/src/transport_ws_pb_js.dart index 265504e..cf35ee1 100644 --- a/lib/src/transport_ws_pb_js.dart +++ b/lib/src/transport_ws_pb_js.dart @@ -415,5 +415,6 @@ final class SpinifyTransport$WS$PB$JS implements ISpinifyTransport { _socket.close(code); else _socket.close(); + //assert(_socket.readyState == 3, 'Socket is not closed'); } } From 60afae91cc39c1c02f0f9ade690a91160e3d4059 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 20 Jul 2024 04:55:49 +0400 Subject: [PATCH 03/40] feat: Improve WebSocket handling and add Protobuf transport support --- .vscode/launch.json | 2 +- lib/src/spinify_impl.dart | 40 ++++++-- test/smoke/create_client.dart | 3 +- test/smoke/smoke_test.dart | 170 +++++++++++++++++++--------------- tool/echo/echo.go | 7 +- 5 files changed, 135 insertions(+), 87 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ff76443..65e3b18 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -163,7 +163,7 @@ "--timeout=5m", "--concurrency=12", "--chain-stack-traces", - "--name=Disconnect_temporarily", + /* "--name=Disconnect_permanent", */ ], "args": [], /* "preLaunchTask": "echo:start", diff --git a/lib/src/spinify_impl.dart b/lib/src/spinify_impl.dart index 618bb9e..dd1af78 100644 --- a/lib/src/spinify_impl.dart +++ b/lib/src/spinify_impl.dart @@ -614,13 +614,19 @@ base mixin SpinifyConnectionMixin @override Future connect(String url) async { - if (state.url == url) return; - final completer = _readyCompleter ??= Completer(); + //if (state.url == url) return; + final completer = _readyCompleter = switch (_readyCompleter) { + Completer value when !value.isCompleted => value, + _ => Completer(), + }; try { if (state.isConnected || state.isConnecting) await disconnect(); } on Object {/* ignore */} + assert(!completer.isCompleted, 'Completer should not be completed'); + assert(state.isDisconnected, 'State should be disconnected'); try { _setState(SpinifyState$Connecting(url: _metrics.reconnectUrl = url)); + assert(state.isConnecting, 'State should be connecting'); // Create new transport. _transport = await _createTransport( @@ -934,12 +940,19 @@ base mixin SpinifyConnectionMixin } @override - Future disconnect() async { - // Disable reconnect because we are disconnecting manually/intentionally. - _metrics.reconnectUrl = null; - _tearDownReconnectTimer(); + Future disconnect() => + _disconnect(code: 1000, reason: 'disconnected by client'); + + /// Disconnect client from the server with optional reconnect and reason. + Future _disconnect( + {int? code, String? reason, bool reconnect = false}) async { + if (!reconnect) { + // Disable reconnect because we are disconnecting manually/intentionally. + _metrics.reconnectUrl = null; + _tearDownReconnectTimer(); + } if (state.isDisconnected) return Future.value(); - await _transport?.disconnect(1000, 'disconnected by client'); + await _transport?.disconnect(code, reason); await _onDisconnected(); } @@ -1015,7 +1028,7 @@ base mixin SpinifyPingPongMixin when pingInterval != null && pingInterval > Duration.zero) { _pingTimer = Timer( pingInterval + config.serverPingDelay, - () { + () async { // Reconnect if no pong received. if (state case SpinifyState$Connected(:String url)) { config.logger?.call( @@ -1028,7 +1041,16 @@ base mixin SpinifyPingPongMixin 'serverPingDelay': config.serverPingDelay, }, ); - connect(url); + try { + await _disconnect( + code: 2, + reason: 'No ping from server', + reconnect: true, + ); + await Future.delayed(Duration.zero); + } finally { + await connect(url); + } } /* disconnect( SpinifyConnectingCode.noPing, diff --git a/test/smoke/create_client.dart b/test/smoke/create_client.dart index 4529077..add33fe 100644 --- a/test/smoke/create_client.dart +++ b/test/smoke/create_client.dart @@ -37,7 +37,7 @@ void _loggerCheckReply(SpinifyLogLevel level, String event, String message, } if (_$prevPeply != null) { expect(() => reply == _$prevPeply, returnsNormally); - expect(reply.compareTo(_$prevPeply!), isNonNegative); + expect(() => reply.compareTo(_$prevPeply!), returnsNormally); } _$prevPeply = reply; } @@ -95,6 +95,7 @@ ISpinify $createClient() => Spinify( min: const Duration(milliseconds: 50), max: const Duration(milliseconds: 150), ), + serverPingDelay: const Duration(milliseconds: 500), logger: _logger, ), ); diff --git a/test/smoke/smoke_test.dart b/test/smoke/smoke_test.dart index 2da2813..6836c46 100644 --- a/test/smoke/smoke_test.dart +++ b/test/smoke/smoke_test.dart @@ -31,82 +31,102 @@ void main() { expect(client.state, isA()); }, timeout: const Timeout(Duration(minutes: 7))); - test('Disconnect_temporarily', () async { - final client = $createClient(); - await client.connect($url); - expect(client.state, isA()); - await client.rpc('disconnect', utf8.encode('reconnect')); - // await client.stream.disconnect().first; - await client.states.disconnected.first; - expect(client.state, isA()); - expect( - client.metrics, - isA() - .having( - (m) => m.connects, - 'connects = 1', - equals(1), - ) - .having( - (m) => m.disconnects, - 'disconnects = 1', - equals(1), - ) - .having( - (m) => m.reconnectUrl, - 'reconnectUrl is set', - isNotNull, - ) - .having( - (m) => m.nextReconnectAt, - 'nextReconnectAt is set', - isNotNull, - ), - ); - await client.states.connecting.first; - await client.states.connected.first; - expect(client.state, isA()); - await Future.delayed(const Duration(milliseconds: 250)); - await client.close(); - expect(client.state, isA()); - }); + test( + 'Disconnect_temporarily', + () async { + final client = $createClient(); + await client.connect($url); + expect(client.state, isA()); + await client.rpc('disconnect', utf8.encode('reconnect')); + // await client.stream.disconnect().first; + await client.states.disconnected.first; + expect(client.state, isA()); + expect( + client.metrics, + isA() + .having( + (m) => m.connects, + 'connects = 1', + equals(1), + ) + .having( + (m) => m.disconnects, + 'disconnects = 1', + equals(1), + ) + .having( + (m) => m.reconnectUrl, + 'reconnectUrl is set', + isNotNull, + ) + .having( + (m) => m.nextReconnectAt, + 'nextReconnectAt is set', + isNotNull, + ), + ); + await client.states.connecting.first; + await client.states.connected.first; + expect(client.state, isA()); + await Future.delayed(const Duration(milliseconds: 250)); + await client.close(); + expect(client.state, isA()); + }, + onPlatform: { + 'browser': [ + const Timeout.factor(2), + ], + }, + ); - test('Disconnect_permanent', () async { - final client = $createClient(); - await client.connect($url); - expect(client.state, isA()); - await client.rpc('disconnect', utf8.encode('permanent')); - await client.states.disconnected.first; - expect(client.state, isA()); - expect( - client.metrics, - isA() - .having( - (m) => m.connects, - 'connects = 1', - equals(1), - ) - .having( - (m) => m.disconnects, - 'disconnects = 1', - equals(1), - ) - .having( - (m) => m.reconnectUrl, - 'reconnectUrl is not set', - isNull, - ) - .having( - (m) => m.nextReconnectAt, - 'nextReconnectAt is not set', - isNull, - ), - ); - await Future.delayed(const Duration(milliseconds: 250)); - expect(client.state, isA()); - await client.close(); - expect(client.state, isA()); - }); + test( + 'Disconnect_permanent', + () async { + final client = $createClient(); + await client.connect($url); + expect(client.state, isA()); + await client.rpc('disconnect', utf8.encode('permanent')); + await client.states.disconnected.first; + expect(client.state, isA()); + expect( + client.metrics, + isA() + .having( + (m) => m.connects, + 'connects = 1', + equals(1), + ) + .having( + (m) => m.disconnects, + 'disconnects = 1', + equals(1), + ) + .having( + (m) => m.reconnectUrl, + 'reconnectUrl is not set', + isNull, + ) + .having( + (m) => m.nextReconnectAt, + 'nextReconnectAt is not set', + isNull, + ), + ); + await Future.delayed(const Duration(milliseconds: 250)); + expect(client.state, isA()); + await client.close(); + expect(client.state, isA()); + }, + onPlatform: { + 'browser': [ + const Skip('Not supported on browsers, yet. ' + 'Because server can not disconnect with code and reason ' + 'and reconnect will happen by ping.'), + // They'll be slow on browsers once it works on them. + const Timeout.factor(2), + ], + }, + ); }); group('Subscriptions', () { diff --git a/tool/echo/echo.go b/tool/echo/echo.go index ff6cee6..5761692 100644 --- a/tool/echo/echo.go +++ b/tool/echo/echo.go @@ -110,6 +110,10 @@ func Centrifuge() (*centrifuge.Node, error) { return centrifuge.ConnectReply{ Data: []byte(`{}`), ClientSideRefresh: false, + PingPongConfig: ¢rifuge.PingPongConfig{ + PingInterval: 2 * time.Second, + PongTimeout: 1 * time.Second, + }, // Subscribe to a personal server-side channel. Subscriptions: map[string]centrifuge.SubscribeOptions{ "#" + cred.UserID: { @@ -216,7 +220,7 @@ func Centrifuge() (*centrifuge.Node, error) { }) client.OnRPC(func(e centrifuge.RPCEvent, cb centrifuge.RPCCallback) { - log.Printf("[user %s] sent RPC, data: %s, method: %s", client.UserID(), string(e.Data), e.Method) + log.Printf("[user %s] sent RPC, method: %s, data: %s", client.UserID(), string(e.Data), e.Method) switch e.Method { case "getCurrentYear": // Return current year. @@ -227,6 +231,7 @@ func Centrifuge() (*centrifuge.Node, error) { case "disconnect": // Disconnect user cb(centrifuge.RPCReply{}, nil) + time.Sleep(50 * time.Millisecond) // <== without this sleep, client will not receive disconnect reply if string(e.Data) == "reconnect" { client.Disconnect(centrifuge.Disconnect{ Code: 3001, From 5352517c85ca6eae9704aa6e4084c0a4f629cd29 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 20 Jul 2024 05:52:06 +0400 Subject: [PATCH 04/40] Example of rpc disconnect --- tool/echo/echo.go | 2 +- tool/echo/index.html | 69 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 tool/echo/index.html diff --git a/tool/echo/echo.go b/tool/echo/echo.go index 5761692..cfedf28 100644 --- a/tool/echo/echo.go +++ b/tool/echo/echo.go @@ -231,7 +231,7 @@ func Centrifuge() (*centrifuge.Node, error) { case "disconnect": // Disconnect user cb(centrifuge.RPCReply{}, nil) - time.Sleep(50 * time.Millisecond) // <== without this sleep, client will not receive disconnect reply + //time.Sleep(50 * time.Millisecond) // <== without this sleep, client will not receive disconnect reply if string(e.Data) == "reconnect" { client.Disconnect(centrifuge.Disconnect{ Code: 3001, diff --git a/tool/echo/index.html b/tool/echo/index.html new file mode 100644 index 0000000..1df2936 --- /dev/null +++ b/tool/echo/index.html @@ -0,0 +1,69 @@ + + + + + + + Centrifuge Client Example + + + + + +

Example

+ + + +

Status: Disconnected

+ + + + + \ No newline at end of file From 844ddd3c5c51a8c99259f9b2e58718fa6a86965e Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 20 Jul 2024 06:08:45 +0400 Subject: [PATCH 05/40] feat: Add support for reconnecting in RPC disconnect method This commit modifies the RPC disconnect method in the `echo.go` file to add support for reconnecting. It checks if the data parameter contains the string "reconnect" and disconnects the client with reconnection if true. Otherwise, it disconnects the client without reconnection. This enhancement improves the flexibility of the RPC disconnect functionality. --- tool/echo/echo.go | 7 +++++-- tool/echo/index.html | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/tool/echo/echo.go b/tool/echo/echo.go index cfedf28..93163df 100644 --- a/tool/echo/echo.go +++ b/tool/echo/echo.go @@ -10,6 +10,7 @@ import ( "os/signal" "slices" "strconv" + "strings" "time" "io" @@ -220,7 +221,7 @@ func Centrifuge() (*centrifuge.Node, error) { }) client.OnRPC(func(e centrifuge.RPCEvent, cb centrifuge.RPCCallback) { - log.Printf("[user %s] sent RPC, method: %s, data: %s", client.UserID(), string(e.Data), e.Method) + log.Printf("[user %s] sent RPC, method: %s, data: %s", client.UserID(), e.Method, string(e.Data)) switch e.Method { case "getCurrentYear": // Return current year. @@ -232,12 +233,14 @@ func Centrifuge() (*centrifuge.Node, error) { // Disconnect user cb(centrifuge.RPCReply{}, nil) //time.Sleep(50 * time.Millisecond) // <== without this sleep, client will not receive disconnect reply - if string(e.Data) == "reconnect" { + if strings.Contains(string(e.Data), "reconnect") { + log.Printf("[user %s] disconnect with reconnection", client.UserID()) client.Disconnect(centrifuge.Disconnect{ Code: 3001, Reason: "disconnect with reconnection", }) } else { + log.Printf("[user %s] disconnect without reconnection", client.UserID()) client.Disconnect(centrifuge.DisconnectForceNoReconnect) } default: diff --git a/tool/echo/index.html b/tool/echo/index.html index 1df2936..65894cf 100644 --- a/tool/echo/index.html +++ b/tool/echo/index.html @@ -17,16 +17,24 @@

Example

-

Status: Disconnected

+ + + + + + diff --git a/benchmark/web/manifest.json b/benchmark/web/manifest.json new file mode 100644 index 0000000..7348a9f --- /dev/null +++ b/benchmark/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "spinifybenchmark", + "short_name": "spinifybenchmark", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Benchmark", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/benchmark/windows/.gitignore b/benchmark/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/benchmark/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/benchmark/windows/CMakeLists.txt b/benchmark/windows/CMakeLists.txt new file mode 100644 index 0000000..6f06d8b --- /dev/null +++ b/benchmark/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(spinifybenchmark LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "spinifybenchmark") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/benchmark/windows/flutter/CMakeLists.txt b/benchmark/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/benchmark/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/benchmark/windows/flutter/generated_plugin_registrant.cc b/benchmark/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/benchmark/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/benchmark/windows/flutter/generated_plugin_registrant.h b/benchmark/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/benchmark/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/benchmark/windows/flutter/generated_plugins.cmake b/benchmark/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b93c4c3 --- /dev/null +++ b/benchmark/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/benchmark/windows/runner/CMakeLists.txt b/benchmark/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/benchmark/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/benchmark/windows/runner/Runner.rc b/benchmark/windows/runner/Runner.rc new file mode 100644 index 0000000..a83c43e --- /dev/null +++ b/benchmark/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "dev.plugfox" "\0" + VALUE "FileDescription", "spinifybenchmark" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "spinifybenchmark" "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 dev.plugfox. All rights reserved." "\0" + VALUE "OriginalFilename", "spinifybenchmark.exe" "\0" + VALUE "ProductName", "spinifybenchmark" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/benchmark/windows/runner/flutter_window.cpp b/benchmark/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/benchmark/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/benchmark/windows/runner/flutter_window.h b/benchmark/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/benchmark/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/benchmark/windows/runner/main.cpp b/benchmark/windows/runner/main.cpp new file mode 100644 index 0000000..2c79bf4 --- /dev/null +++ b/benchmark/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"spinifybenchmark", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/benchmark/windows/runner/resource.h b/benchmark/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/benchmark/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/benchmark/windows/runner/resources/app_icon.ico b/benchmark/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/benchmark/windows/runner/runner.exe.manifest b/benchmark/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/benchmark/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/benchmark/windows/runner/utils.cpp b/benchmark/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/benchmark/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/benchmark/windows/runner/utils.h b/benchmark/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/benchmark/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/benchmark/windows/runner/win32_window.cpp b/benchmark/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/benchmark/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/benchmark/windows/runner/win32_window.h b/benchmark/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/benchmark/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ From 015a4d0563fc9c0148c8820012c8ac7196ebd937 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 16 Aug 2024 18:56:48 +0400 Subject: [PATCH 21/40] refactor: Update launch configuration for benchmark in VSCode This commit updates the launch configuration in the `.vscode/launch.json` file to set up debugging for the benchmark app instead of the example app. The `name` field is changed to "[Flutter] Benchmark" and the `cwd` field is updated to "${workspaceFolder}/benchmark". This change ensures that the correct app is launched and debugged when running the benchmark. --- .vscode/launch.json | 12 +- benchmark/lib/main.dart | 142 +++------------ benchmark/lib/src/benchmark_app.dart | 252 +++++++++++++++++++++++++++ benchmark/pubspec.yaml | 82 ++------- benchmark/test/widget_test.dart | 31 +--- 5 files changed, 294 insertions(+), 225 deletions(-) create mode 100644 benchmark/lib/src/benchmark_app.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index 8266e9f..d601280 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,12 +1,12 @@ { "version": "0.2.0", "configurations": [ - /* { - "name": "[Flutter] Example (Local)", + { + "name": "[Flutter] Benchmark", "request": "launch", "type": "dart", "flutterMode": "debug", - "cwd": "${workspaceFolder}/example", + "cwd": "${workspaceFolder}/benchmark", "program": "lib/main.dart", "env": { "ENVIRONMENT": "local" @@ -14,10 +14,8 @@ "console": "debugConsole", "runTestsOnDevice": false, "toolArgs": [], - "args": [ - "--dart-define-from-file=config/local.json" - ] - }, */ + "args": [] + }, /* { "name": "[Flutter] Example (Development)", "request": "launch", diff --git a/benchmark/lib/main.dart b/benchmark/lib/main.dart index 8e94089..ca2014b 100644 --- a/benchmark/lib/main.dart +++ b/benchmark/lib/main.dart @@ -1,125 +1,31 @@ -import 'package:flutter/material.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); +import 'dart:async'; - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; +import 'package:flutter/material.dart'; +import 'package:l/l.dart'; +import 'package:spinifybenchmark/src/benchmark_app.dart'; - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; +void main() => _appZone(() async { + runApp(const BenchmarkApp()); }); - } - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), +/// Catch all application errors and logs. +void _appZone(FutureOr Function() fn) => l.capture( + () => runZonedGuarded( + () => fn(), + l.e, ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), + const LogOptions( + handlePrint: true, + messageFormatting: _messageFormatting, + outputInRelease: false, + printColors: true, ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. ); - } -} + +/// Formats the log message. +Object _messageFormatting(LogMessage log) => + '${_timeFormat(log.timestamp)} | ${log.message}'; + +/// Formats the time. +String _timeFormat(DateTime time) => + '${time.hour}:${time.minute.toString().padLeft(2, '0')}'; diff --git a/benchmark/lib/src/benchmark_app.dart b/benchmark/lib/src/benchmark_app.dart new file mode 100644 index 0000000..071e238 --- /dev/null +++ b/benchmark/lib/src/benchmark_app.dart @@ -0,0 +1,252 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class BenchmarkApp extends StatefulWidget { + const BenchmarkApp({super.key}); + + @override + State createState() => _BenchmarkAppState(); +} + +class _BenchmarkAppState extends State { + final ValueNotifier themeMode = ValueNotifier( + PlatformDispatcher.instance.platformBrightness == Brightness.dark + ? ThemeMode.dark + : ThemeMode.light); + + @override + void initState() { + super.initState(); + themeMode.addListener(_onChanged); + } + + @override + void dispose() { + themeMode.removeListener(_onChanged); + super.dispose(); + } + + void _onChanged() => setState(() {}); + + void toggleTheme() => themeMode.value = switch (themeMode.value) { + ThemeMode.dark => ThemeMode.light, + ThemeMode.light || _ => ThemeMode.dark, + }; + + @override + Widget build(BuildContext context) => MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Benchmark', + themeMode: themeMode.value, + theme: switch (themeMode.value) { + ThemeMode.dark => ThemeData.dark(), + ThemeMode.light || _ => ThemeData.light(), + }, + home: _BenchmarkScaffold( + themeMode: themeMode, + ), + ); +} + +enum Library { spinify, centrifuge } + +class BenchmarkController with ChangeNotifier { + /// Library to use for the benchmark. + final ValueNotifier library = + ValueNotifier(Library.centrifuge); + + /// WebSocket endpoint to connect to. + final TextEditingController endpoint = + TextEditingController(text: 'ws://localhost:8000/connection/websocket'); + + /// Size in bytes of the payload to send/receive. + final ValueNotifier payloadSize = ValueNotifier(1024 * 1024); + + /// Number of messages to send/receive. + final ValueNotifier duration = ValueNotifier(1000); + + Future start() async {} + + @override + void dispose() { + endpoint.dispose(); + super.dispose(); + } +} + +class _BenchmarkScaffold extends StatefulWidget { + const _BenchmarkScaffold({ + required this.themeMode, + super.key, // ignore: unused_element + }); + + final ValueListenable themeMode; + + @override + State<_BenchmarkScaffold> createState() => _BenchmarkScaffoldState(); +} + +class _BenchmarkScaffoldState extends State<_BenchmarkScaffold> + with SingleTickerProviderStateMixin { + final BenchmarkController controller = BenchmarkController(); + late final TabController tabBarController; + + @override + void initState() { + tabBarController = TabController(length: 2, vsync: this); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + tabBarController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Benchmark'), + actions: [ + ValueListenableBuilder( + valueListenable: widget.themeMode, + builder: (context, mode, _) => IconButton( + icon: switch (mode) { + ThemeMode.dark => Icon(Icons.light_mode), + ThemeMode.light => Icon(Icons.dark_mode), + ThemeMode.system => Icon(Icons.auto_awesome), + }, + onPressed: () => context + .findAncestorStateOfType<_BenchmarkAppState>() + ?.toggleTheme(), + )), + SizedBox(width: 8), + ], + ), + bottomNavigationBar: ListenableBuilder( + listenable: tabBarController, + builder: (context, _) => BottomNavigationBar( + currentIndex: tabBarController.index, + onTap: (index) => tabBarController.animateTo(index), + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.speed), + label: 'Benchmark', + ), + /* BottomNavigationBarItem( + icon: Icon(Icons.device_unknown), + label: 'Unknown', + ), */ + BottomNavigationBarItem( + icon: Icon(Icons.help), + label: 'Help', + ), + ], + )), + body: SafeArea( + child: TabBarView( + controller: tabBarController, + children: [ + Align( + alignment: Alignment.topCenter, + child: _BenchmarkTab( + controller: controller, + ), + ), + /* Center( + child: Text('Unknown'), + ), */ + Center( + child: Text('Help'), + ), + ], + ), + ), + ); +} + +class _BenchmarkTab extends StatelessWidget { + const _BenchmarkTab({ + required this.controller, + super.key, // ignore: unused_element + }); + + final BenchmarkController controller; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ValueListenableBuilder( + valueListenable: controller.library, + builder: (context, library, _) => SegmentedButton( + selected: {library}, + segments: >[ + ButtonSegment( + value: Library.spinify, + label: Text('Spinify'), + ), + ButtonSegment( + value: Library.centrifuge, + label: Text('Centrifuge'), + ), + ], + ), + ), + const SizedBox(height: 16), + TextField( + controller: controller.endpoint, + decoration: const InputDecoration( + labelText: 'Endpoint', + hintText: 'ws://localhost:8000/connection/websocket', + ), + ), + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: controller.payloadSize, + builder: (context, size, _) => Slider( + value: size.toDouble(), + min: 0, + max: 1024 * 1024 * 10, + divisions: 100, + label: switch (size) { + 0 => 'Not set', + 1 => '1 byte', + >= 1024 * 1024 * 1024 => '${size ~/ 1024 ~/ 1024 ~/ 100}GB', + >= 1024 * 1024 => '${size ~/ 1024 ~/ 1024}MB', + >= 1024 => '${size ~/ 1024}KB', + _ => '$size bytes', + }, + onChanged: (value) => + controller.payloadSize.value = value.toInt(), + ), + ), + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: controller.duration, + builder: (context, duration, _) => Slider( + value: duration.toDouble(), + min: 1, + max: 1000000, + divisions: 100, + label: switch (duration) { + 0 => 'Not set', + 1 => '1 message', + >= 1000000 => '${duration ~/ 1000000}M messages', + >= 1000 => '${duration ~/ 1000}k messages', + _ => '$duration messages', + }, + onChanged: (value) => controller.duration.value = value.toInt(), + ), + ), + /* Spacer(), + Spacer(), */ + ], + ), + ); +} diff --git a/benchmark/pubspec.yaml b/benchmark/pubspec.yaml index 8dc0ec4..5e60e74 100644 --- a/benchmark/pubspec.yaml +++ b/benchmark/pubspec.yaml @@ -1,90 +1,32 @@ name: spinifybenchmark -description: "Benchmark" -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +description: "Spinify Benchmark" + +publish_to: 'none' -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: ^3.5.0 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. + # UI cupertino_icons: ^1.0.8 + # Centrifuge clients + centrifuge: ^0.14.0 + spinify: + path: ../ + + # Utilities + l: ^5.0.0-pre.2 + dev_dependencies: flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^4.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package diff --git a/benchmark/test/widget_test.dart b/benchmark/test/widget_test.dart index a0c7872..ab73b3a 100644 --- a/benchmark/test/widget_test.dart +++ b/benchmark/test/widget_test.dart @@ -1,30 +1 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:spinifybenchmark/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} +void main() {} From ebbc5d27b7199640037245d2335c8a09c393da9a Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 16 Aug 2024 19:00:26 +0400 Subject: [PATCH 22/40] refactor: Update payload size and message count UI in benchmark app This commit updates the UI of the benchmark app to improve the user experience when setting the payload size and message count. It adds labels for the payload size and message count sliders, making it easier for users to understand their purpose. Additionally, it adjusts the layout and padding of the UI elements for better visual consistency. These changes enhance the usability of the benchmark app and make it more intuitive for users to configure the payload size and message count. --- benchmark/lib/src/benchmark_app.dart | 46 +++++++++++++++++++--------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/benchmark/lib/src/benchmark_app.dart b/benchmark/lib/src/benchmark_app.dart index 071e238..899e984 100644 --- a/benchmark/lib/src/benchmark_app.dart +++ b/benchmark/lib/src/benchmark_app.dart @@ -63,7 +63,7 @@ class BenchmarkController with ChangeNotifier { final ValueNotifier payloadSize = ValueNotifier(1024 * 1024); /// Number of messages to send/receive. - final ValueNotifier duration = ValueNotifier(1000); + final ValueNotifier messageCount = ValueNotifier(1000); Future start() async {} @@ -199,14 +199,24 @@ class _BenchmarkTab extends StatelessWidget { ), ), const SizedBox(height: 16), - TextField( - controller: controller.endpoint, - decoration: const InputDecoration( - labelText: 'Endpoint', - hintText: 'ws://localhost:8000/connection/websocket', + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextField( + controller: controller.endpoint, + decoration: const InputDecoration( + labelText: 'Endpoint', + hintText: 'ws://localhost:8000/connection/websocket', + ), ), ), const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + 'Payload size', + style: Theme.of(context).textTheme.labelSmall, + ), + ), ValueListenableBuilder( valueListenable: controller.payloadSize, builder: (context, size, _) => Slider( @@ -227,21 +237,29 @@ class _BenchmarkTab extends StatelessWidget { ), ), const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + 'Message count', + style: Theme.of(context).textTheme.labelSmall, + ), + ), ValueListenableBuilder( - valueListenable: controller.duration, - builder: (context, duration, _) => Slider( - value: duration.toDouble(), + valueListenable: controller.messageCount, + builder: (context, count, _) => Slider( + value: count.toDouble(), min: 1, max: 1000000, divisions: 100, - label: switch (duration) { + label: switch (count) { 0 => 'Not set', 1 => '1 message', - >= 1000000 => '${duration ~/ 1000000}M messages', - >= 1000 => '${duration ~/ 1000}k messages', - _ => '$duration messages', + >= 1000000 => '${count ~/ 1000000}M messages', + >= 1000 => '${count ~/ 1000}k messages', + _ => '$count messages', }, - onChanged: (value) => controller.duration.value = value.toInt(), + onChanged: (value) => + controller.messageCount.value = value.toInt(), ), ), /* Spacer(), From ecdff0daf696e5b3f78018f4e374ae9736bf0757 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 16 Aug 2024 19:11:54 +0400 Subject: [PATCH 23/40] refactor: Update BenchmarkController to use abstract base class This commit updates the `BenchmarkController` class to use an abstract base class called `BenchmarkControllerBase`. This change allows for better code organization and separation of concerns. The `BenchmarkControllerBase` class contains common properties and methods that are shared by different benchmark implementations. By using an abstract base class, it becomes easier to add new benchmark implementations in the future. This refactor improves the maintainability and extensibility of the codebase. --- benchmark/lib/src/benchmark_app.dart | 58 +++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/benchmark/lib/src/benchmark_app.dart b/benchmark/lib/src/benchmark_app.dart index 899e984..f2ac5c1 100644 --- a/benchmark/lib/src/benchmark_app.dart +++ b/benchmark/lib/src/benchmark_app.dart @@ -50,7 +50,7 @@ class _BenchmarkAppState extends State { enum Library { spinify, centrifuge } -class BenchmarkController with ChangeNotifier { +abstract base class BenchmarkControllerBase with ChangeNotifier { /// Library to use for the benchmark. final ValueNotifier library = ValueNotifier(Library.centrifuge); @@ -65,7 +65,32 @@ class BenchmarkController with ChangeNotifier { /// Number of messages to send/receive. final ValueNotifier messageCount = ValueNotifier(1000); - Future start() async {} + /// Number of sent messages. + int get sent => _sent; + int _sent = 0; + + /// Number of received messages. + int get received => _received; + int _received = 0; + + /// Number of failed messages. + int get failed => _failed; + int _failed = 0; + + /// Total number of messages to send/receive. + int get total => _total; + int _total = 0; + + /// Progress of the benchmark in percent. + int get progress => + _total == 0 ? 0 : (((_received + _failed) * 100) ~/ _total).clamp(0, 100); + + /// Duration of the benchmark in milliseconds. + int get duration => _duration; + int _duration = 0; + + /// Start the benchmark. + Future start(); @override void dispose() { @@ -74,6 +99,27 @@ class BenchmarkController with ChangeNotifier { } } +mixin SpinifyBenchmark on ChangeNotifier { + Future startSpinify() async {} +} + +mixin CentrifugeBenchmark on ChangeNotifier { + Future startCentrifuge() async {} +} + +final class BenchmarkControllerImpl extends BenchmarkControllerBase + with SpinifyBenchmark, CentrifugeBenchmark { + @override + Future start() { + switch (library.value) { + case Library.spinify: + return startSpinify(); + case Library.centrifuge: + return startCentrifuge(); + } + } +} + class _BenchmarkScaffold extends StatefulWidget { const _BenchmarkScaffold({ required this.themeMode, @@ -88,7 +134,7 @@ class _BenchmarkScaffold extends StatefulWidget { class _BenchmarkScaffoldState extends State<_BenchmarkScaffold> with SingleTickerProviderStateMixin { - final BenchmarkController controller = BenchmarkController(); + final BenchmarkControllerImpl controller = BenchmarkControllerImpl(); late final TabController tabBarController; @override @@ -172,7 +218,7 @@ class _BenchmarkTab extends StatelessWidget { super.key, // ignore: unused_element }); - final BenchmarkController controller; + final BenchmarkControllerImpl controller; @override Widget build(BuildContext context) => Padding( @@ -262,8 +308,8 @@ class _BenchmarkTab extends StatelessWidget { controller.messageCount.value = value.toInt(), ), ), - /* Spacer(), - Spacer(), */ + Spacer(), + Spacer(), ], ), ); From fc64b4ecf679edb8f0fe89ece3e2d5f0f522b745 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 16 Aug 2024 19:56:18 +0400 Subject: [PATCH 24/40] refactor: Update payload size and message count UI in benchmark app --- benchmark/lib/src/benchmark_app.dart | 166 ++++++++++++++++++++------- 1 file changed, 126 insertions(+), 40 deletions(-) diff --git a/benchmark/lib/src/benchmark_app.dart b/benchmark/lib/src/benchmark_app.dart index f2ac5c1..545fdf7 100644 --- a/benchmark/lib/src/benchmark_app.dart +++ b/benchmark/lib/src/benchmark_app.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:spinify/spinify.dart'; class BenchmarkApp extends StatefulWidget { const BenchmarkApp({super.key}); @@ -53,7 +56,7 @@ enum Library { spinify, centrifuge } abstract base class BenchmarkControllerBase with ChangeNotifier { /// Library to use for the benchmark. final ValueNotifier library = - ValueNotifier(Library.centrifuge); + ValueNotifier(Library.spinify); /// WebSocket endpoint to connect to. final TextEditingController endpoint = @@ -65,6 +68,10 @@ abstract base class BenchmarkControllerBase with ChangeNotifier { /// Number of messages to send/receive. final ValueNotifier messageCount = ValueNotifier(1000); + /// Number of pending messages. + int get pending => _pending; + int _pending = 0; + /// Number of sent messages. int get sent => _sent; int _sent = 0; @@ -99,23 +106,80 @@ abstract base class BenchmarkControllerBase with ChangeNotifier { } } -mixin SpinifyBenchmark on ChangeNotifier { - Future startSpinify() async {} +base mixin SpinifyBenchmark on BenchmarkControllerBase { + Future startSpinify({void Function(Object error)? onError}) async { + final Spinify client; + try { + client = Spinify(); + await client.connect(endpoint.text); + } on Object catch (e) { + onError?.call(e); + return; + } + + final payload = + List.generate(payloadSize.value, (index) => index % 256); + final stopwatch = Stopwatch()..start(); + void pump() { + _duration = stopwatch.elapsedMilliseconds; + notifyListeners(); + } + + _total = messageCount.value; + StreamSubscription? subscription; + Completer? completer; + try { + _pending = _sent = _received = _failed = 0; + subscription = + client.stream.publication(channel: 'benchmark').listen((event) { + if (event.data.length == payload.length) { + _received++; + } else { + _failed++; + } + _duration = stopwatch.elapsedMilliseconds; + completer?.complete(); + }); + for (var i = 0; i < _total; i++) { + try { + _pending++; + pump(); + completer = Completer(); + await client.publish('benchmark', payload); + _sent++; + pump(); + await completer.future.timeout(const Duration(seconds: 5)); + pump(); + } on Object catch (e) { + _failed++; + onError?.call(e); + pump(); + } + } + } on Object catch (e) { + onError?.call(e); + return; + } finally { + subscription?.cancel().ignore(); + stopwatch.stop(); + client.disconnect().ignore(); + } + } } -mixin CentrifugeBenchmark on ChangeNotifier { - Future startCentrifuge() async {} +base mixin CentrifugeBenchmark on BenchmarkControllerBase { + Future startCentrifuge({void Function(Object error)? onError}) async {} } final class BenchmarkControllerImpl extends BenchmarkControllerBase with SpinifyBenchmark, CentrifugeBenchmark { @override - Future start() { + Future start({void Function(Object error)? onError}) { switch (library.value) { case Library.spinify: - return startSpinify(); + return startSpinify(onError: onError); case Library.centrifuge: - return startCentrifuge(); + return startCentrifuge(onError: onError); } } } @@ -156,40 +220,42 @@ class _BenchmarkScaffoldState extends State<_BenchmarkScaffold> title: const Text('Benchmark'), actions: [ ValueListenableBuilder( - valueListenable: widget.themeMode, - builder: (context, mode, _) => IconButton( - icon: switch (mode) { - ThemeMode.dark => Icon(Icons.light_mode), - ThemeMode.light => Icon(Icons.dark_mode), - ThemeMode.system => Icon(Icons.auto_awesome), - }, - onPressed: () => context - .findAncestorStateOfType<_BenchmarkAppState>() - ?.toggleTheme(), - )), + valueListenable: widget.themeMode, + builder: (context, mode, _) => IconButton( + icon: switch (mode) { + ThemeMode.dark => Icon(Icons.light_mode), + ThemeMode.light => Icon(Icons.dark_mode), + ThemeMode.system => Icon(Icons.auto_awesome), + }, + onPressed: () => context + .findAncestorStateOfType<_BenchmarkAppState>() + ?.toggleTheme(), + ), + ), SizedBox(width: 8), ], ), bottomNavigationBar: ListenableBuilder( - listenable: tabBarController, - builder: (context, _) => BottomNavigationBar( - currentIndex: tabBarController.index, - onTap: (index) => tabBarController.animateTo(index), - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.speed), - label: 'Benchmark', - ), - /* BottomNavigationBarItem( - icon: Icon(Icons.device_unknown), - label: 'Unknown', - ), */ - BottomNavigationBarItem( - icon: Icon(Icons.help), - label: 'Help', - ), - ], - )), + listenable: tabBarController, + builder: (context, _) => BottomNavigationBar( + currentIndex: tabBarController.index, + onTap: (index) => tabBarController.animateTo(index), + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.speed), + label: 'Benchmark', + ), + /* BottomNavigationBarItem( + icon: Icon(Icons.device_unknown), + label: 'Unknown', + ), */ + BottomNavigationBarItem( + icon: Icon(Icons.help), + label: 'Help', + ), + ], + ), + ), body: SafeArea( child: TabBarView( controller: tabBarController, @@ -201,8 +267,8 @@ class _BenchmarkScaffoldState extends State<_BenchmarkScaffold> ), ), /* Center( - child: Text('Unknown'), - ), */ + child: Text('Unknown'), + ), */ Center( child: Text('Help'), ), @@ -231,6 +297,8 @@ class _BenchmarkTab extends StatelessWidget { ValueListenableBuilder( valueListenable: controller.library, builder: (context, library, _) => SegmentedButton( + onSelectionChanged: (value) => + controller.library.value = value.firstOrNull ?? library, selected: {library}, segments: >[ ButtonSegment( @@ -309,6 +377,24 @@ class _BenchmarkTab extends StatelessWidget { ), ), Spacer(), + ListenableBuilder( + listenable: controller, + builder: (context, _) => Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Pending: ${controller.pending}'), + Text('Sent: ${controller.sent}'), + Text('Received: ${controller.received}'), + Text('Failed: ${controller.failed}'), + Text('Total: ${controller.total}'), + Text('Progress: ${controller.progress}%'), + Text('Duration: ${controller.duration}ms'), + ], + ), + ), + Spacer(), Spacer(), ], ), From 0bd4e0b0c1bebb9b357e2918ecd39210d5169136 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 16 Aug 2024 20:27:28 +0400 Subject: [PATCH 25/40] refactor: Update payload size and message count UI in benchmark app --- benchmark/lib/src/benchmark_app.dart | 259 ++++++++++++++++----------- 1 file changed, 157 insertions(+), 102 deletions(-) diff --git a/benchmark/lib/src/benchmark_app.dart b/benchmark/lib/src/benchmark_app.dart index 545fdf7..798d31a 100644 --- a/benchmark/lib/src/benchmark_app.dart +++ b/benchmark/lib/src/benchmark_app.dart @@ -68,6 +68,9 @@ abstract base class BenchmarkControllerBase with ChangeNotifier { /// Number of messages to send/receive. final ValueNotifier messageCount = ValueNotifier(1000); + /// Whether the benchmark is running. + final ValueNotifier isRunning = ValueNotifier(false); + /// Number of pending messages. int get pending => _pending; int _pending = 0; @@ -108,12 +111,14 @@ abstract base class BenchmarkControllerBase with ChangeNotifier { base mixin SpinifyBenchmark on BenchmarkControllerBase { Future startSpinify({void Function(Object error)? onError}) async { + isRunning.value = true; final Spinify client; try { client = Spinify(); await client.connect(endpoint.text); } on Object catch (e) { onError?.call(e); + isRunning.value = false; return; } @@ -158,6 +163,7 @@ base mixin SpinifyBenchmark on BenchmarkControllerBase { } } on Object catch (e) { onError?.call(e); + isRunning.value = false; return; } finally { subscription?.cancel().ignore(); @@ -287,116 +293,165 @@ class _BenchmarkTab extends StatelessWidget { final BenchmarkControllerImpl controller; @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ValueListenableBuilder( - valueListenable: controller.library, - builder: (context, library, _) => SegmentedButton( - onSelectionChanged: (value) => - controller.library.value = value.firstOrNull ?? library, - selected: {library}, - segments: >[ - ButtonSegment( - value: Library.spinify, - label: Text('Spinify'), - ), - ButtonSegment( - value: Library.centrifuge, - label: Text('Centrifuge'), - ), - ], - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextField( - controller: controller.endpoint, - decoration: const InputDecoration( - labelText: 'Endpoint', - hintText: 'ws://localhost:8000/connection/websocket', - ), - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - 'Payload size', - style: Theme.of(context).textTheme.labelSmall, - ), - ), - ValueListenableBuilder( - valueListenable: controller.payloadSize, - builder: (context, size, _) => Slider( - value: size.toDouble(), - min: 0, - max: 1024 * 1024 * 10, - divisions: 100, - label: switch (size) { - 0 => 'Not set', - 1 => '1 byte', - >= 1024 * 1024 * 1024 => '${size ~/ 1024 ~/ 1024 ~/ 100}GB', - >= 1024 * 1024 => '${size ~/ 1024 ~/ 1024}MB', - >= 1024 => '${size ~/ 1024}KB', - _ => '$size bytes', - }, - onChanged: (value) => - controller.payloadSize.value = value.toInt(), + Widget build(BuildContext context) => Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ValueListenableBuilder( + valueListenable: controller.isRunning, + builder: (context, running, child) => AbsorbPointer( + absorbing: running, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: running ? 0.5 : 1, + child: child, ), ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - 'Message count', - style: Theme.of(context).textTheme.labelSmall, - ), - ), - ValueListenableBuilder( - valueListenable: controller.messageCount, - builder: (context, count, _) => Slider( - value: count.toDouble(), - min: 1, - max: 1000000, - divisions: 100, - label: switch (count) { - 0 => 'Not set', - 1 => '1 message', - >= 1000000 => '${count ~/ 1000000}M messages', - >= 1000 => '${count ~/ 1000}k messages', - _ => '$count messages', - }, - onChanged: (value) => - controller.messageCount.value = value.toInt(), - ), - ), - Spacer(), - ListenableBuilder( - listenable: controller, - builder: (context, _) => Column( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Pending: ${controller.pending}'), - Text('Sent: ${controller.sent}'), - Text('Received: ${controller.received}'), - Text('Failed: ${controller.failed}'), - Text('Total: ${controller.total}'), - Text('Progress: ${controller.progress}%'), - Text('Duration: ${controller.duration}ms'), + ValueListenableBuilder( + valueListenable: controller.library, + builder: (context, library, _) => SegmentedButton( + onSelectionChanged: (value) => controller.library.value = + value.firstOrNull ?? library, + selected: {library}, + segments: >[ + ButtonSegment( + value: Library.spinify, + label: Text('Spinify'), + ), + ButtonSegment( + value: Library.centrifuge, + label: Text('Centrifuge'), + ), + ], + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextField( + controller: controller.endpoint, + decoration: const InputDecoration( + labelText: 'Endpoint', + hintText: 'ws://localhost:8000/connection/websocket', + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + 'Payload size', + style: Theme.of(context).textTheme.labelSmall, + ), + ), + ValueListenableBuilder( + valueListenable: controller.payloadSize, + builder: (context, size, _) => Slider( + value: size.toDouble(), + min: 0, + max: 1024 * 1024 * 10, + divisions: 100, + label: switch (size) { + 0 => 'Not set', + 1 => '1 byte', + >= 1024 * 1024 * 1024 => + '${size ~/ 1024 ~/ 1024 ~/ 100}GB', + >= 1024 * 1024 => '${size ~/ 1024 ~/ 1024}MB', + >= 1024 => '${size ~/ 1024}KB', + _ => '$size bytes', + }, + onChanged: (value) => + controller.payloadSize.value = value.toInt(), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + 'Message count', + style: Theme.of(context).textTheme.labelSmall, + ), + ), + ValueListenableBuilder( + valueListenable: controller.messageCount, + builder: (context, count, _) => Slider( + value: count.toDouble(), + min: 1, + max: 1000000, + divisions: 100, + label: switch (count) { + 0 => 'Not set', + 1 => '1 message', + >= 1000000 => '${count ~/ 1000000}M messages', + >= 1000 => '${count ~/ 1000}k messages', + _ => '$count messages', + }, + onChanged: (value) => + controller.messageCount.value = value.toInt(), + ), + ), ], ), ), - Spacer(), - Spacer(), - ], - ), + ), + Spacer(), + ListenableBuilder( + listenable: controller, + builder: (context, _) => Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ValueListenableBuilder( + valueListenable: controller.isRunning, + builder: (context, running, child) => IconButton( + iconSize: 64, + icon: Icon(running ? Icons.timer : Icons.play_arrow, + color: running ? Colors.grey : Colors.red), + onPressed: running + ? null + : () { + final messenger = + ScaffoldMessenger.maybeOf(context); + controller.start( + onError: (error) => messenger + ?..clearSnackBars() + ..showSnackBar( + SnackBar( + content: Text('$error'), + backgroundColor: Colors.red, + ), + ), + ); + }, + ), + ), + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Pending: ${controller.pending}'), + Text('Sent: ${controller.sent}'), + Text('Received: ${controller.received}'), + Text('Failed: ${controller.failed}'), + Text('Total: ${controller.total}'), + Text('Progress: ${controller.progress}%'), + Text('Duration: ${controller.duration}ms'), + ], + ), + ], + ), + ), + Spacer(), + ], ); } From 6c62057bd4caf3ec6c01f91fa5ce6ce721725344 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 16 Aug 2024 20:36:16 +0400 Subject: [PATCH 26/40] refactor: Update benchmark app UI and status handling This commit refactors the benchmark app to improve the user interface and status handling. It updates the UI to enhance the user experience when setting the payload size and message count, adding labels for the sliders and adjusting the layout for better visual consistency. Additionally, it introduces a new status property in the `BenchmarkControllerBase` class to track the status of the benchmark, allowing for better communication with the user. These changes improve the usability and clarity of the benchmark app. --- benchmark/lib/src/benchmark_app.dart | 49 ++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/benchmark/lib/src/benchmark_app.dart b/benchmark/lib/src/benchmark_app.dart index 798d31a..6da83de 100644 --- a/benchmark/lib/src/benchmark_app.dart +++ b/benchmark/lib/src/benchmark_app.dart @@ -71,6 +71,10 @@ abstract base class BenchmarkControllerBase with ChangeNotifier { /// Whether the benchmark is running. final ValueNotifier isRunning = ValueNotifier(false); + /// Status of the benchmark. + String get status => _status; + String _status = ''; + /// Number of pending messages. int get pending => _pending; int _pending = 0; @@ -111,32 +115,42 @@ abstract base class BenchmarkControllerBase with ChangeNotifier { base mixin SpinifyBenchmark on BenchmarkControllerBase { Future startSpinify({void Function(Object error)? onError}) async { + _duration = 0; isRunning.value = true; + final stopwatch = Stopwatch()..start(); + void pump(String message) { + _status = message; + _duration = stopwatch.elapsedMilliseconds; + notifyListeners(); + } + final Spinify client; try { + pump('Connecting to ${endpoint.text}...'); client = Spinify(); await client.connect(endpoint.text); + pump('Connected to ${endpoint.text}.'); } on Object catch (e) { + pump('Failed to connect to ${endpoint.text}. ${e}'); onError?.call(e); + stopwatch.stop(); isRunning.value = false; return; } final payload = List.generate(payloadSize.value, (index) => index % 256); - final stopwatch = Stopwatch()..start(); - void pump() { - _duration = stopwatch.elapsedMilliseconds; - notifyListeners(); - } _total = messageCount.value; - StreamSubscription? subscription; + SpinifyClientSubscription subscription; + StreamSubscription? streamSubscription; Completer? completer; try { - _pending = _sent = _received = _failed = 0; - subscription = - client.stream.publication(channel: 'benchmark').listen((event) { + _pending = _sent = _received = _failed = _duration = 0; + pump('Subscribing to channel "benchmark"...'); + subscription = client.newSubscription('benchmark'); + await subscription.subscribe(); + streamSubscription = subscription.stream.publication().listen((event) { if (event.data.length == payload.length) { _received++; } else { @@ -148,25 +162,31 @@ base mixin SpinifyBenchmark on BenchmarkControllerBase { for (var i = 0; i < _total; i++) { try { _pending++; - pump(); + pump('Sending message $i...'); completer = Completer(); await client.publish('benchmark', payload); _sent++; - pump(); + pump('Sent message $i.'); await completer.future.timeout(const Duration(seconds: 5)); - pump(); + pump('Received message $i.'); } on Object catch (e) { _failed++; onError?.call(e); - pump(); + pump('Failed to send message $i.'); } } + pump('Unsubscribing from channel "benchmark"...'); + await client.removeSubscription(subscription); + pump('Disconnecting from ${endpoint.text}...'); + await client.disconnect(); + pump('Done.'); } on Object catch (e) { onError?.call(e); + pump('Failed. ${e}'); isRunning.value = false; return; } finally { - subscription?.cancel().ignore(); + streamSubscription?.cancel().ignore(); stopwatch.stop(); client.disconnect().ignore(); } @@ -446,6 +466,7 @@ class _BenchmarkTab extends StatelessWidget { Text('Total: ${controller.total}'), Text('Progress: ${controller.progress}%'), Text('Duration: ${controller.duration}ms'), + Text('Status: ${controller.status}'), ], ), ], From 1f55191f6daadcf995c50b24d3db99ad4e0db1d2 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 17 Aug 2024 12:17:23 +0400 Subject: [PATCH 27/40] refactor: Update benchmark app UI and status handling --- benchmark/analysis_options.yaml | 252 ++++++++++++-- benchmark/lib/src/benchmark_app.dart | 351 +------------------- benchmark/lib/src/benchmark_controller.dart | 210 ++++++++++++ benchmark/lib/src/benchmark_tab.dart | 175 ++++++++++ 4 files changed, 623 insertions(+), 365 deletions(-) create mode 100644 benchmark/lib/src/benchmark_controller.dart create mode 100644 benchmark/lib/src/benchmark_tab.dart diff --git a/benchmark/analysis_options.yaml b/benchmark/analysis_options.yaml index 0d29021..16c6e8d 100644 --- a/benchmark/analysis_options.yaml +++ b/benchmark/analysis_options.yaml @@ -1,28 +1,234 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + # Build + - "build/**" + # Tests + - "test/**.mocks.dart" + - ".test_coverage.dart" + - "coverage/**" + # Assets + - "assets/**" + # Generated + - "lib/src/common/localization/generated/**" + - "lib/src/common/constants/pubspec.yaml.g.dart" + - "lib/src/common/model/generated/**" + - "**.g.dart" + - "**.gql.dart" + - "**.freezed.dart" + - "**.config.dart" + - "**.mocks.dart" + - "**.gen.dart" + - "**.pb.dart" + - "**.pbenum.dart" + - "**.pbjson.dart" + # Flutter Version Manager + - ".fvm/**" + # Tools + #- "tool/**" + - "scripts/**" + - ".dart_tool/**" + # Platform + - "ios/**" + - "android/**" + - "web/**" + - "macos/**" + - "windows/**" + - "linux/**" + + # Enable the following options to enable strong mode. + language: + strict-casts: true + strict-raw-types: true + strict-inference: true + + errors: + # Allow having TODOs in the code + todo: ignore + + # Info + directives_ordering: info + always_declare_return_types: info + + # Warning + unsafe_html: warning + missing_return: warning + missing_required_param: warning + no_logic_in_create_state: warning + empty_catches: warning + + # Error + always_use_package_imports: error + avoid_relative_lib_imports: error + avoid_slow_async_io: error + avoid_types_as_parameter_names: error + valid_regexps: error + always_require_non_null_named_parameters: error + linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + # Public packages + #public_member_api_docs: true + #lines_longer_than_80_chars: true + + # Enabling rules + always_use_package_imports: true + avoid_relative_lib_imports: true + + # Disable rules + sort_pub_dependencies: false + prefer_relative_imports: false + prefer_final_locals: false + avoid_escaping_inner_quotes: false + curly_braces_in_flow_control_structures: false + + # Enabled + use_named_constants: true + unnecessary_constructor_name: true + sort_constructors_first: true + exhaustive_cases: true + sort_unnamed_constructors_first: true + type_literal_in_constant_pattern: true + always_put_required_named_parameters_first: true + avoid_annotating_with_dynamic: true + avoid_bool_literals_in_conditional_expressions: true + avoid_double_and_int_checks: true + avoid_field_initializers_in_const_classes: true + avoid_implementing_value_types: true + avoid_js_rounded_ints: true + avoid_print: true + avoid_renaming_method_parameters: true + avoid_returning_null_for_void: true + avoid_single_cascade_in_expression_statements: true + avoid_slow_async_io: true + avoid_unnecessary_containers: true + avoid_unused_constructor_parameters: true + avoid_void_async: true + await_only_futures: true + cancel_subscriptions: true + cascade_invocations: true + close_sinks: true + control_flow_in_finally: true + empty_statements: true + collection_methods_unrelated_type: true + join_return_with_assignment: true + leading_newlines_in_multiline_strings: true + literal_only_boolean_expressions: true + missing_whitespace_between_adjacent_strings: true + no_adjacent_strings_in_list: true + no_logic_in_create_state: true + no_runtimeType_toString: true + only_throw_errors: true + overridden_fields: true + package_names: true + package_prefixed_library_names: true + parameter_assignments: true + prefer_asserts_in_initializer_lists: true + prefer_asserts_with_message: true + prefer_const_constructors: true + prefer_const_constructors_in_immutables: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + prefer_constructors_over_static_methods: true + prefer_expression_function_bodies: true + prefer_final_in_for_each: true + prefer_foreach: true + prefer_if_elements_to_conditional_expressions: true + prefer_inlined_adds: true + prefer_int_literals: true + prefer_is_not_operator: true + prefer_null_aware_operators: true + prefer_typing_uninitialized_variables: true + prefer_void_to_null: true + provide_deprecation_message: true + sized_box_for_whitespace: true + sort_child_properties_last: true + test_types_in_equals: true + throw_in_finally: true + unnecessary_null_aware_assignments: true + unnecessary_overrides: true + unnecessary_parenthesis: true + unnecessary_raw_strings: true + unnecessary_statements: true + unnecessary_string_escapes: true + unnecessary_string_interpolations: true + unsafe_html: true + use_full_hex_values_for_flutter_colors: true + use_raw_strings: true + use_string_buffers: true + valid_regexps: true + void_checks: true + + # Pedantic 1.9.0 + always_declare_return_types: true + annotate_overrides: true + avoid_empty_else: true + avoid_init_to_null: true + avoid_null_checks_in_equality_operators: true + avoid_return_types_on_setters: true + avoid_shadowing_type_parameters: true + avoid_types_as_parameter_names: true + camel_case_extensions: true + empty_catches: true + empty_constructor_bodies: true + library_names: true + library_prefixes: true + no_duplicate_case_values: true + null_closures: true + omit_local_variable_types: true + prefer_adjacent_string_concatenation: true + prefer_collection_literals: true + prefer_conditional_assignment: true + prefer_contains: true + prefer_final_fields: true + prefer_for_elements_to_map_fromIterable: true + prefer_generic_function_type_aliases: true + prefer_if_null_operators: true + prefer_is_empty: true + prefer_is_not_empty: true + prefer_iterable_whereType: true + prefer_single_quotes: true + prefer_spread_collections: true + recursive_getters: true + slash_for_doc_comments: true + type_init_formals: true + unawaited_futures: true + unnecessary_const: true + unnecessary_new: true + unnecessary_null_in_if_null_operators: true + unnecessary_this: true + unrelated_type_equality_checks: true + use_function_type_syntax_for_parameters: true + use_rethrow_when_possible: true -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + # Effective_dart 1.2.0 + camel_case_types: true + file_names: true + non_constant_identifier_names: true + constant_identifier_names: true + directives_ordering: true + package_api_docs: true + implementation_imports: true + prefer_interpolation_to_compose_strings: true + unnecessary_brace_in_string_interps: true + avoid_function_literals_in_foreach_calls: true + prefer_function_declarations_over_variables: true + unnecessary_lambdas: true + unnecessary_getters_setters: true + prefer_initializing_formals: true + avoid_catches_without_on_clauses: true + avoid_catching_errors: true + use_to_and_as_if_applicable: true + one_member_abstracts: true + avoid_classes_with_only_static_members: true + prefer_mixin: true + use_setters_to_change_properties: true + avoid_setters_without_getters: true + avoid_returning_this: true + type_annotate_public_apis: true + avoid_types_on_closure_parameters: true + avoid_private_typedef_functions: true + avoid_positional_boolean_parameters: true + hash_and_equals: true + avoid_equals_and_hash_code_on_mutable_classes: true \ No newline at end of file diff --git a/benchmark/lib/src/benchmark_app.dart b/benchmark/lib/src/benchmark_app.dart index 6da83de..5a7abd7 100644 --- a/benchmark/lib/src/benchmark_app.dart +++ b/benchmark/lib/src/benchmark_app.dart @@ -1,8 +1,7 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:spinify/spinify.dart'; +import 'package:spinifybenchmark/src/benchmark_controller.dart'; +import 'package:spinifybenchmark/src/benchmark_tab.dart'; class BenchmarkApp extends StatefulWidget { const BenchmarkApp({super.key}); @@ -51,165 +50,6 @@ class _BenchmarkAppState extends State { ); } -enum Library { spinify, centrifuge } - -abstract base class BenchmarkControllerBase with ChangeNotifier { - /// Library to use for the benchmark. - final ValueNotifier library = - ValueNotifier(Library.spinify); - - /// WebSocket endpoint to connect to. - final TextEditingController endpoint = - TextEditingController(text: 'ws://localhost:8000/connection/websocket'); - - /// Size in bytes of the payload to send/receive. - final ValueNotifier payloadSize = ValueNotifier(1024 * 1024); - - /// Number of messages to send/receive. - final ValueNotifier messageCount = ValueNotifier(1000); - - /// Whether the benchmark is running. - final ValueNotifier isRunning = ValueNotifier(false); - - /// Status of the benchmark. - String get status => _status; - String _status = ''; - - /// Number of pending messages. - int get pending => _pending; - int _pending = 0; - - /// Number of sent messages. - int get sent => _sent; - int _sent = 0; - - /// Number of received messages. - int get received => _received; - int _received = 0; - - /// Number of failed messages. - int get failed => _failed; - int _failed = 0; - - /// Total number of messages to send/receive. - int get total => _total; - int _total = 0; - - /// Progress of the benchmark in percent. - int get progress => - _total == 0 ? 0 : (((_received + _failed) * 100) ~/ _total).clamp(0, 100); - - /// Duration of the benchmark in milliseconds. - int get duration => _duration; - int _duration = 0; - - /// Start the benchmark. - Future start(); - - @override - void dispose() { - endpoint.dispose(); - super.dispose(); - } -} - -base mixin SpinifyBenchmark on BenchmarkControllerBase { - Future startSpinify({void Function(Object error)? onError}) async { - _duration = 0; - isRunning.value = true; - final stopwatch = Stopwatch()..start(); - void pump(String message) { - _status = message; - _duration = stopwatch.elapsedMilliseconds; - notifyListeners(); - } - - final Spinify client; - try { - pump('Connecting to ${endpoint.text}...'); - client = Spinify(); - await client.connect(endpoint.text); - pump('Connected to ${endpoint.text}.'); - } on Object catch (e) { - pump('Failed to connect to ${endpoint.text}. ${e}'); - onError?.call(e); - stopwatch.stop(); - isRunning.value = false; - return; - } - - final payload = - List.generate(payloadSize.value, (index) => index % 256); - - _total = messageCount.value; - SpinifyClientSubscription subscription; - StreamSubscription? streamSubscription; - Completer? completer; - try { - _pending = _sent = _received = _failed = _duration = 0; - pump('Subscribing to channel "benchmark"...'); - subscription = client.newSubscription('benchmark'); - await subscription.subscribe(); - streamSubscription = subscription.stream.publication().listen((event) { - if (event.data.length == payload.length) { - _received++; - } else { - _failed++; - } - _duration = stopwatch.elapsedMilliseconds; - completer?.complete(); - }); - for (var i = 0; i < _total; i++) { - try { - _pending++; - pump('Sending message $i...'); - completer = Completer(); - await client.publish('benchmark', payload); - _sent++; - pump('Sent message $i.'); - await completer.future.timeout(const Duration(seconds: 5)); - pump('Received message $i.'); - } on Object catch (e) { - _failed++; - onError?.call(e); - pump('Failed to send message $i.'); - } - } - pump('Unsubscribing from channel "benchmark"...'); - await client.removeSubscription(subscription); - pump('Disconnecting from ${endpoint.text}...'); - await client.disconnect(); - pump('Done.'); - } on Object catch (e) { - onError?.call(e); - pump('Failed. ${e}'); - isRunning.value = false; - return; - } finally { - streamSubscription?.cancel().ignore(); - stopwatch.stop(); - client.disconnect().ignore(); - } - } -} - -base mixin CentrifugeBenchmark on BenchmarkControllerBase { - Future startCentrifuge({void Function(Object error)? onError}) async {} -} - -final class BenchmarkControllerImpl extends BenchmarkControllerBase - with SpinifyBenchmark, CentrifugeBenchmark { - @override - Future start({void Function(Object error)? onError}) { - switch (library.value) { - case Library.spinify: - return startSpinify(onError: onError); - case Library.centrifuge: - return startCentrifuge(onError: onError); - } - } -} - class _BenchmarkScaffold extends StatefulWidget { const _BenchmarkScaffold({ required this.themeMode, @@ -224,7 +64,7 @@ class _BenchmarkScaffold extends StatefulWidget { class _BenchmarkScaffoldState extends State<_BenchmarkScaffold> with SingleTickerProviderStateMixin { - final BenchmarkControllerImpl controller = BenchmarkControllerImpl(); + final IBenchmarkController controller = BenchmarkControllerImpl(); late final TabController tabBarController; @override @@ -249,16 +89,16 @@ class _BenchmarkScaffoldState extends State<_BenchmarkScaffold> valueListenable: widget.themeMode, builder: (context, mode, _) => IconButton( icon: switch (mode) { - ThemeMode.dark => Icon(Icons.light_mode), - ThemeMode.light => Icon(Icons.dark_mode), - ThemeMode.system => Icon(Icons.auto_awesome), + ThemeMode.dark => const Icon(Icons.light_mode), + ThemeMode.light => const Icon(Icons.dark_mode), + ThemeMode.system => const Icon(Icons.auto_awesome), }, onPressed: () => context .findAncestorStateOfType<_BenchmarkAppState>() ?.toggleTheme(), ), ), - SizedBox(width: 8), + const SizedBox(width: 8), ], ), bottomNavigationBar: ListenableBuilder( @@ -288,14 +128,14 @@ class _BenchmarkScaffoldState extends State<_BenchmarkScaffold> children: [ Align( alignment: Alignment.topCenter, - child: _BenchmarkTab( + child: BenchmarkTab( controller: controller, ), ), /* Center( child: Text('Unknown'), ), */ - Center( + const Center( child: Text('Help'), ), ], @@ -303,176 +143,3 @@ class _BenchmarkScaffoldState extends State<_BenchmarkScaffold> ), ); } - -class _BenchmarkTab extends StatelessWidget { - const _BenchmarkTab({ - required this.controller, - super.key, // ignore: unused_element - }); - - final BenchmarkControllerImpl controller; - - @override - Widget build(BuildContext context) => Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ValueListenableBuilder( - valueListenable: controller.isRunning, - builder: (context, running, child) => AbsorbPointer( - absorbing: running, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: running ? 0.5 : 1, - child: child, - ), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ValueListenableBuilder( - valueListenable: controller.library, - builder: (context, library, _) => SegmentedButton( - onSelectionChanged: (value) => controller.library.value = - value.firstOrNull ?? library, - selected: {library}, - segments: >[ - ButtonSegment( - value: Library.spinify, - label: Text('Spinify'), - ), - ButtonSegment( - value: Library.centrifuge, - label: Text('Centrifuge'), - ), - ], - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextField( - controller: controller.endpoint, - decoration: const InputDecoration( - labelText: 'Endpoint', - hintText: 'ws://localhost:8000/connection/websocket', - ), - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - 'Payload size', - style: Theme.of(context).textTheme.labelSmall, - ), - ), - ValueListenableBuilder( - valueListenable: controller.payloadSize, - builder: (context, size, _) => Slider( - value: size.toDouble(), - min: 0, - max: 1024 * 1024 * 10, - divisions: 100, - label: switch (size) { - 0 => 'Not set', - 1 => '1 byte', - >= 1024 * 1024 * 1024 => - '${size ~/ 1024 ~/ 1024 ~/ 100}GB', - >= 1024 * 1024 => '${size ~/ 1024 ~/ 1024}MB', - >= 1024 => '${size ~/ 1024}KB', - _ => '$size bytes', - }, - onChanged: (value) => - controller.payloadSize.value = value.toInt(), - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - 'Message count', - style: Theme.of(context).textTheme.labelSmall, - ), - ), - ValueListenableBuilder( - valueListenable: controller.messageCount, - builder: (context, count, _) => Slider( - value: count.toDouble(), - min: 1, - max: 1000000, - divisions: 100, - label: switch (count) { - 0 => 'Not set', - 1 => '1 message', - >= 1000000 => '${count ~/ 1000000}M messages', - >= 1000 => '${count ~/ 1000}k messages', - _ => '$count messages', - }, - onChanged: (value) => - controller.messageCount.value = value.toInt(), - ), - ), - ], - ), - ), - ), - Spacer(), - ListenableBuilder( - listenable: controller, - builder: (context, _) => Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ValueListenableBuilder( - valueListenable: controller.isRunning, - builder: (context, running, child) => IconButton( - iconSize: 64, - icon: Icon(running ? Icons.timer : Icons.play_arrow, - color: running ? Colors.grey : Colors.red), - onPressed: running - ? null - : () { - final messenger = - ScaffoldMessenger.maybeOf(context); - controller.start( - onError: (error) => messenger - ?..clearSnackBars() - ..showSnackBar( - SnackBar( - content: Text('$error'), - backgroundColor: Colors.red, - ), - ), - ); - }, - ), - ), - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Pending: ${controller.pending}'), - Text('Sent: ${controller.sent}'), - Text('Received: ${controller.received}'), - Text('Failed: ${controller.failed}'), - Text('Total: ${controller.total}'), - Text('Progress: ${controller.progress}%'), - Text('Duration: ${controller.duration}ms'), - Text('Status: ${controller.status}'), - ], - ), - ], - ), - ), - Spacer(), - ], - ); -} diff --git a/benchmark/lib/src/benchmark_controller.dart b/benchmark/lib/src/benchmark_controller.dart new file mode 100644 index 0000000..d08f16f --- /dev/null +++ b/benchmark/lib/src/benchmark_controller.dart @@ -0,0 +1,210 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:spinify/spinify.dart'; + +enum Library { spinify, centrifuge } + +abstract interface class IBenchmarkController implements Listenable { + /// Library to use for the benchmark. + abstract final ValueNotifier library; + + /// WebSocket endpoint to connect to. + abstract final TextEditingController endpoint; + + /// Size in bytes of the payload to send/receive. + abstract final ValueNotifier payloadSize; + + /// Number of messages to send/receive. + abstract final ValueNotifier messageCount; + + /// Whether the benchmark is running. + abstract final ValueListenable isRunning; + + /// Status of the benchmark. + String get status; + + /// Number of pending messages. + int get pending; + + /// Number of sent messages. + int get sent; + + /// Number of received messages. + int get received; + + /// Number of failed messages. + int get failed; + + /// Total number of messages to send/receive. + int get total; + + /// Progress of the benchmark in percent. + int get progress; + + /// Duration of the benchmark in milliseconds. + int get duration; + + /// Start the benchmark. + Future start({void Function(Object error)? onError}); + + /// Dispose of the controller. + void dispose(); +} + +abstract base class BenchmarkControllerBase + with ChangeNotifier + implements IBenchmarkController { + @override + final ValueNotifier library = + ValueNotifier(Library.spinify); + + @override + final TextEditingController endpoint = + TextEditingController(text: 'ws://localhost:8000/connection/websocket'); + + @override + final ValueNotifier payloadSize = ValueNotifier(1024 * 1024); + + @override + final ValueNotifier messageCount = ValueNotifier(1000); + + @override + final ValueNotifier isRunning = ValueNotifier(false); + + @override + String get status => _status; + String _status = ''; + + @override + int get pending => _pending; + int _pending = 0; + + @override + int get sent => _sent; + int _sent = 0; + + @override + int get received => _received; + int _received = 0; + + @override + int get failed => _failed; + int _failed = 0; + + @override + int get total => _total; + int _total = 0; + + @override + int get progress => + _total == 0 ? 0 : (((_received + _failed) * 100) ~/ _total).clamp(0, 100); + + @override + int get duration => _duration; + int _duration = 0; + + @override + void dispose() { + endpoint.dispose(); + super.dispose(); + } +} + +base mixin SpinifyBenchmark on BenchmarkControllerBase { + Future startSpinify({void Function(Object error)? onError}) async { + _duration = 0; + isRunning.value = true; + final stopwatch = Stopwatch()..start(); + void pump(String message) { + _status = message; + _duration = stopwatch.elapsedMilliseconds; + notifyListeners(); + } + + final Spinify client; + try { + pump('Connecting to ${endpoint.text}...'); + client = Spinify(); + await client.connect(endpoint.text); + pump('Connected to ${endpoint.text}.'); + } on Object catch (e) { + pump('Failed to connect to ${endpoint.text}. $e'); + onError?.call(e); + stopwatch.stop(); + isRunning.value = false; + return; + } + + final payload = + List.generate(payloadSize.value, (index) => index % 256); + + _total = messageCount.value; + SpinifyClientSubscription subscription; + StreamSubscription? streamSubscription; + Completer? completer; + try { + _pending = _sent = _received = _failed = _duration = 0; + pump('Subscribing to channel "benchmark"...'); + subscription = client.newSubscription('benchmark'); + await subscription.subscribe(); + streamSubscription = subscription.stream.publication().listen((event) { + if (event.data.length == payload.length) { + _received++; + } else { + _failed++; + } + _duration = stopwatch.elapsedMilliseconds; + completer?.complete(); + }); + for (var i = 0; i < _total; i++) { + try { + _pending++; + pump('Sending message $i...'); + completer = Completer(); + await client.publish('benchmark', payload); + _sent++; + pump('Sent message $i.'); + await completer.future.timeout(const Duration(seconds: 5)); + pump('Received message $i.'); + } on Object catch (e) { + _failed++; + onError?.call(e); + pump('Failed to send message $i.'); + } + } + pump('Unsubscribing from channel "benchmark"...'); + await client.removeSubscription(subscription); + pump('Disconnecting from ${endpoint.text}...'); + await client.disconnect(); + pump('Done.'); + } on Object catch (e) { + onError?.call(e); + pump('Failed. $e'); + isRunning.value = false; + return; + } finally { + streamSubscription?.cancel().ignore(); + stopwatch.stop(); + client.disconnect().ignore(); + } + } +} + +base mixin CentrifugeBenchmark on BenchmarkControllerBase { + Future startCentrifuge({void Function(Object error)? onError}) async {} +} + +final class BenchmarkControllerImpl extends BenchmarkControllerBase + with SpinifyBenchmark, CentrifugeBenchmark { + @override + Future start({void Function(Object error)? onError}) { + switch (library.value) { + case Library.spinify: + return startSpinify(onError: onError); + case Library.centrifuge: + return startCentrifuge(onError: onError); + } + } +} diff --git a/benchmark/lib/src/benchmark_tab.dart b/benchmark/lib/src/benchmark_tab.dart new file mode 100644 index 0000000..91e3c0c --- /dev/null +++ b/benchmark/lib/src/benchmark_tab.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:spinifybenchmark/src/benchmark_controller.dart'; + +class BenchmarkTab extends StatelessWidget { + const BenchmarkTab({ + required this.controller, + super.key, // ignore: unused_element + }); + + final IBenchmarkController controller; + + @override + Widget build(BuildContext context) => Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ValueListenableBuilder( + valueListenable: controller.isRunning, + builder: (context, running, child) => AbsorbPointer( + absorbing: running, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: running ? 0.5 : 1, + child: child, + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ValueListenableBuilder( + valueListenable: controller.library, + builder: (context, library, _) => SegmentedButton( + onSelectionChanged: (value) => controller.library.value = + value.firstOrNull ?? library, + selected: {library}, + segments: const >[ + ButtonSegment( + value: Library.spinify, + label: Text('Spinify'), + ), + ButtonSegment( + value: Library.centrifuge, + label: Text('Centrifuge'), + ), + ], + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextField( + controller: controller.endpoint, + decoration: const InputDecoration( + labelText: 'Endpoint', + hintText: 'ws://localhost:8000/connection/websocket', + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + 'Payload size', + style: Theme.of(context).textTheme.labelSmall, + ), + ), + ValueListenableBuilder( + valueListenable: controller.payloadSize, + builder: (context, size, _) => Slider( + value: size.toDouble(), + min: 0, + max: 1024 * 1024 * 10, + divisions: 100, + label: switch (size) { + 0 => 'Not set', + 1 => '1 byte', + >= 1024 * 1024 * 1024 => + '${size ~/ 1024 ~/ 1024 ~/ 100}GB', + >= 1024 * 1024 => '${size ~/ 1024 ~/ 1024}MB', + >= 1024 => '${size ~/ 1024}KB', + _ => '$size bytes', + }, + onChanged: (value) => + controller.payloadSize.value = value.toInt(), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + 'Message count', + style: Theme.of(context).textTheme.labelSmall, + ), + ), + ValueListenableBuilder( + valueListenable: controller.messageCount, + builder: (context, count, _) => Slider( + value: count.toDouble(), + min: 1, + max: 1000000, + divisions: 100, + label: switch (count) { + 0 => 'Not set', + 1 => '1 message', + >= 1000000 => '${count ~/ 1000000}M messages', + >= 1000 => '${count ~/ 1000}k messages', + _ => '$count messages', + }, + onChanged: (value) => + controller.messageCount.value = value.toInt(), + ), + ), + ], + ), + ), + ), + const Spacer(), + ListenableBuilder( + listenable: controller, + builder: (context, _) => Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ValueListenableBuilder( + valueListenable: controller.isRunning, + builder: (context, running, child) => IconButton( + iconSize: 64, + icon: Icon(running ? Icons.timer : Icons.play_arrow, + color: running ? Colors.grey : Colors.red), + onPressed: running + ? null + : () { + final messenger = + ScaffoldMessenger.maybeOf(context); + controller.start( + onError: (error) => messenger + ?..clearSnackBars() + ..showSnackBar( + SnackBar( + content: Text('$error'), + backgroundColor: Colors.red, + ), + ), + ); + }, + ), + ), + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Pending: ${controller.pending}'), + Text('Sent: ${controller.sent}'), + Text('Received: ${controller.received}'), + Text('Failed: ${controller.failed}'), + Text('Total: ${controller.total}'), + Text('Progress: ${controller.progress}%'), + Text('Duration: ${controller.duration}ms'), + Text('Status: ${controller.status}'), + ], + ), + ], + ), + ), + const Spacer(), + ], + ); +} From 837daed745c015733596ecff0f63d5f60934dec2 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 17 Aug 2024 14:02:19 +0400 Subject: [PATCH 28/40] ```text refactor: Add constants for JWT token and WebSocket endpoint This commit adds two constants to the benchmark app: `tokenHmacSecretKey` for the JWT token HMAC key and `defaultEndpoint` for the default WebSocket connection endpoint. These constants provide a centralized and easily configurable way to manage these values throughout the app. By using constants, it improves code readability and maintainability. --- benchmark/docker-compose.yml | 42 ++++ benchmark/lib/src/benchmark_controller.dart | 91 ++++++-- benchmark/lib/src/benchmark_tab.dart | 238 ++++++++++++++------ benchmark/lib/src/constant.dart | 5 + 4 files changed, 278 insertions(+), 98 deletions(-) create mode 100644 benchmark/docker-compose.yml create mode 100644 benchmark/lib/src/constant.dart diff --git a/benchmark/docker-compose.yml b/benchmark/docker-compose.yml new file mode 100644 index 0000000..04b4e63 --- /dev/null +++ b/benchmark/docker-compose.yml @@ -0,0 +1,42 @@ +# Docker Compose configuration file for running Centrifugo benchmark. +# docker-compose up -d +# docker-compose down + +version: "3.9" + +services: + centrifugo-benchmark: + container_name: centrifugo-benchmark + image: centrifugo/centrifugo:v5 + restart: unless-stopped + command: centrifugo + tty: true + ports: + - 8000:8000 + environment: + - "CENTRIFUGO_ADMIN=true" + - "CENTRIFUGO_TOKEN_HMAC_SECRET_KEY=80e88856-fe08-4a01-b9fc-73d1d03c2eee" + - "CENTRIFUGO_ADMIN_PASSWORD=6cec4cc2-960d-4e4a-b650-0cbd4bbf0530" + - "CENTRIFUGO_ADMIN_SECRET=70957aac-555b-4bce-b9b8-53ada3a8029e" + - "CENTRIFUGO_API_KEY=8aba9113-d67a-41c6-818a-27aaaaeb64e7" + - "CENTRIFUGO_ALLOWED_ORIGINS=*" + - "CENTRIFUGO_HEALTH=true" + - "CENTRIFUGO_HISTORY_SIZE=10" + - "CENTRIFUGO_HISTORY_TTL=300s" + - "CENTRIFUGO_FORCE_RECOVERY=true" + - "CENTRIFUGO_ALLOW_PUBLISH_FOR_CLIENT=true" + - "CENTRIFUGO_ALLOW_SUBSCRIBE_FOR_CLIENT=true" + - "CENTRIFUGO_ALLOW_SUBSCRIBE_FOR_ANONYMOUS=true" + - "CENTRIFUGO_ALLOW_PUBLISH_FOR_SUBSCRIBER=true" + - "CENTRIFUGO_ALLOW_PUBLISH_FOR_ANONYMOUS=true" + - "CENTRIFUGO_ALLOW_USER_LIMITED_CHANNELS=true" + - "CENTRIFUGO_LOG_LEVEL=debug" + healthcheck: + test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:8000/health"] + interval: 3s + timeout: 3s + retries: 3 + ulimits: + nofile: + soft: 65535 + hard: 65535 diff --git a/benchmark/lib/src/benchmark_controller.dart b/benchmark/lib/src/benchmark_controller.dart index d08f16f..e2fd81d 100644 --- a/benchmark/lib/src/benchmark_controller.dart +++ b/benchmark/lib/src/benchmark_controller.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:spinify/spinify.dart'; +import 'package:spinifybenchmark/src/constant.dart'; enum Library { spinify, centrifuge } @@ -31,9 +32,15 @@ abstract interface class IBenchmarkController implements Listenable { /// Number of sent messages. int get sent; + /// Number of bytes sent. + int get sentBytes; + /// Number of received messages. int get received; + /// Number of bytes received. + int get receivedBytes; + /// Number of failed messages. int get failed; @@ -46,6 +53,12 @@ abstract interface class IBenchmarkController implements Listenable { /// Duration of the benchmark in milliseconds. int get duration; + /// Number of messages per second. + int get messagePerSecond; + + /// Number of bytes per second. + int get bytesPerSecond; + /// Start the benchmark. Future start({void Function(Object error)? onError}); @@ -56,16 +69,23 @@ abstract interface class IBenchmarkController implements Listenable { abstract base class BenchmarkControllerBase with ChangeNotifier implements IBenchmarkController { + Future _getToken() => Future.value(SpinifyJWT( + sub: '1', + exp: DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch, + iss: 'benchmark', + aud: 'benchmark', + ).encode(tokenHmacSecretKey)); + @override final ValueNotifier library = ValueNotifier(Library.spinify); @override final TextEditingController endpoint = - TextEditingController(text: 'ws://localhost:8000/connection/websocket'); + TextEditingController(text: defaultEndpoint); @override - final ValueNotifier payloadSize = ValueNotifier(1024 * 1024); + final ValueNotifier payloadSize = ValueNotifier(255); @override final ValueNotifier messageCount = ValueNotifier(1000); @@ -85,10 +105,18 @@ abstract base class BenchmarkControllerBase int get sent => _sent; int _sent = 0; + @override + int get sentBytes => _sentBytes; + int _sentBytes = 0; + @override int get received => _received; int _received = 0; + @override + int get receivedBytes => _receivedBytes; + int _receivedBytes = 0; + @override int get failed => _failed; int _failed = 0; @@ -105,6 +133,14 @@ abstract base class BenchmarkControllerBase int get duration => _duration; int _duration = 0; + @override + int get messagePerSecond => _messagePerSecond; + int _messagePerSecond = 0; + + @override + int get bytesPerSecond => _bytesPerSecond; + int _bytesPerSecond = 0; + @override void dispose() { endpoint.dispose(); @@ -114,6 +150,9 @@ abstract base class BenchmarkControllerBase base mixin SpinifyBenchmark on BenchmarkControllerBase { Future startSpinify({void Function(Object error)? onError}) async { + // 65510 bytes + final payload = + List.generate(payloadSize.value, (index) => index % 256); _duration = 0; isRunning.value = true; final stopwatch = Stopwatch()..start(); @@ -125,33 +164,34 @@ base mixin SpinifyBenchmark on BenchmarkControllerBase { final Spinify client; try { - pump('Connecting to ${endpoint.text}...'); - client = Spinify(); + pump('Connecting to centrifugo'); + client = Spinify(config: SpinifyConfig(getToken: _getToken)); await client.connect(endpoint.text); - pump('Connected to ${endpoint.text}.'); + if (!client.state.isConnected) throw Exception('Failed to connect'); + pump('Connected to ${endpoint.text}'); } on Object catch (e) { - pump('Failed to connect to ${endpoint.text}. $e'); + pump('Failed to connect'); onError?.call(e); stopwatch.stop(); isRunning.value = false; return; } - final payload = - List.generate(payloadSize.value, (index) => index % 256); - _total = messageCount.value; SpinifyClientSubscription subscription; StreamSubscription? streamSubscription; Completer? completer; try { _pending = _sent = _received = _failed = _duration = 0; - pump('Subscribing to channel "benchmark"...'); - subscription = client.newSubscription('benchmark'); + pump('Subscribing to channel "benchmark"'); + subscription = client.newSubscription('benchmark#1'); await subscription.subscribe(); + if (!subscription.state.isSubscribed) + throw Exception('Failed to subscribe to channel "benchmark"'); streamSubscription = subscription.stream.publication().listen((event) { if (event.data.length == payload.length) { _received++; + _receivedBytes += payload.length; } else { _failed++; } @@ -161,32 +201,37 @@ base mixin SpinifyBenchmark on BenchmarkControllerBase { for (var i = 0; i < _total; i++) { try { _pending++; - pump('Sending message $i...'); + pump('Sending message $i'); completer = Completer(); - await client.publish('benchmark', payload); + await subscription.publish(payload); _sent++; - pump('Sent message $i.'); + _sentBytes += payload.length; + pump('Sent message $i'); await completer.future.timeout(const Duration(seconds: 5)); - pump('Received message $i.'); + pump('Received message $i'); } on Object catch (e) { _failed++; onError?.call(e); - pump('Failed to send message $i.'); + pump('Failed to send message $i'); + } finally { + _pending--; + if (stopwatch.elapsed.inMilliseconds case int ms when ms > 0) { + _messagePerSecond = (_sent + _received) * 1000 ~/ ms; + _bytesPerSecond = (_sentBytes + _receivedBytes) * 1000 ~/ ms; + } } } - pump('Unsubscribing from channel "benchmark"...'); - await client.removeSubscription(subscription); - pump('Disconnecting from ${endpoint.text}...'); - await client.disconnect(); - pump('Done.'); + pump('Unsubscribing from channel "benchmark"'); + if (subscription.state.isSubscribed) await subscription.unsubscribe(); + pump('Done'); } on Object catch (e) { onError?.call(e); pump('Failed. $e'); - isRunning.value = false; return; } finally { - streamSubscription?.cancel().ignore(); stopwatch.stop(); + isRunning.value = false; + streamSubscription?.cancel().ignore(); client.disconnect().ignore(); } } diff --git a/benchmark/lib/src/benchmark_tab.dart b/benchmark/lib/src/benchmark_tab.dart index 91e3c0c..66754bb 100644 --- a/benchmark/lib/src/benchmark_tab.dart +++ b/benchmark/lib/src/benchmark_tab.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:spinifybenchmark/src/benchmark_controller.dart'; +import 'package:spinifybenchmark/src/constant.dart'; class BenchmarkTab extends StatelessWidget { const BenchmarkTab({ @@ -9,11 +11,25 @@ class BenchmarkTab extends StatelessWidget { final IBenchmarkController controller; + static String _formatBytes(int bytes) => switch (bytes) { + 0 => '0 bytes', + 1 => '1 byte', + >= 1024 * 1024 * 1024 => '${bytes ~/ 1024 ~/ 1024 ~/ 100}GB', + >= 1024 * 1024 => '${bytes ~/ 1024 ~/ 1024}MB', + >= 1024 => '${bytes ~/ 1024}KB', + _ => '$bytes bytes', + }; + + static String _formatMs(int ms) => switch (ms) { + 0 => '0ms', + >= 1000 * 60 * 60 => '${ms ~/ 1000 ~/ 60 ~/ 60}h', + >= 1000 * 60 => '${ms ~/ 1000 ~/ 60}m', + >= 1000 => '${ms ~/ 1000}s', + _ => '${ms}ms', + }; + @override - Widget build(BuildContext context) => Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + Widget build(BuildContext context) => ListView( children: [ ValueListenableBuilder( valueListenable: controller.isRunning, @@ -32,38 +48,88 @@ class BenchmarkTab extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - ValueListenableBuilder( - valueListenable: controller.library, - builder: (context, library, _) => SegmentedButton( - onSelectionChanged: (value) => controller.library.value = - value.firstOrNull ?? library, - selected: {library}, - segments: const >[ - ButtonSegment( - value: Library.spinify, - label: Text('Spinify'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: ValueListenableBuilder( + valueListenable: controller.library, + builder: (context, library, _) => SegmentedButton( + onSelectionChanged: (value) => controller + .library.value = value.firstOrNull ?? library, + selected: {library}, + segments: const >[ + ButtonSegment( + value: Library.spinify, + label: Text('Spinify'), + ), + ButtonSegment( + value: Library.centrifuge, + label: Text('Centrifuge'), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4, + runAlignment: WrapAlignment.start, + verticalDirection: VerticalDirection.down, + runSpacing: 4, + children: [ + IconButton( + icon: const Icon(Icons.copy), + onPressed: () async { + final messenger = + ScaffoldMessenger.maybeOf(context); + await Clipboard.setData( + const ClipboardData(text: tokenHmacSecretKey)); + messenger + ?..clearSnackBars() + ..showSnackBar( + const SnackBar( + content: Text( + 'Copied HMAC secret key to clipboard'), + duration: Duration(seconds: 5), + ), + ); + }, + ), + Flexible( + fit: FlexFit.tight, + child: Text( + 'Token HMAC Secret Key:', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelLarge, + ), ), - ButtonSegment( - value: Library.centrifuge, - label: Text('Centrifuge'), + SelectableText( + tokenHmacSecretKey, + maxLines: 1, + style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), const SizedBox(height: 16), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 24), child: TextField( controller: controller.endpoint, decoration: const InputDecoration( labelText: 'Endpoint', - hintText: 'ws://localhost:8000/connection/websocket', + hintText: defaultEndpoint, ), ), ), const SizedBox(height: 16), Padding( - padding: const EdgeInsets.only(left: 16), + padding: const EdgeInsets.symmetric(horizontal: 24), child: Text( 'Payload size', style: Theme.of(context).textTheme.labelSmall, @@ -73,25 +139,17 @@ class BenchmarkTab extends StatelessWidget { valueListenable: controller.payloadSize, builder: (context, size, _) => Slider( value: size.toDouble(), - min: 0, - max: 1024 * 1024 * 10, + min: 1, + max: 65510, divisions: 100, - label: switch (size) { - 0 => 'Not set', - 1 => '1 byte', - >= 1024 * 1024 * 1024 => - '${size ~/ 1024 ~/ 1024 ~/ 100}GB', - >= 1024 * 1024 => '${size ~/ 1024 ~/ 1024}MB', - >= 1024 => '${size ~/ 1024}KB', - _ => '$size bytes', - }, + label: _formatBytes(size), onChanged: (value) => controller.payloadSize.value = value.toInt(), ), ), const SizedBox(height: 16), Padding( - padding: const EdgeInsets.only(left: 16), + padding: const EdgeInsets.symmetric(horizontal: 24), child: Text( 'Message count', style: Theme.of(context).textTheme.labelSmall, @@ -102,7 +160,7 @@ class BenchmarkTab extends StatelessWidget { builder: (context, count, _) => Slider( value: count.toDouble(), min: 1, - max: 1000000, + max: 10000, divisions: 100, label: switch (count) { 0 => 'Not set', @@ -119,57 +177,87 @@ class BenchmarkTab extends StatelessWidget { ), ), ), - const Spacer(), + const Divider(height: 48), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: ListenableBuilder( + listenable: controller, + builder: (context, _) => LinearProgressIndicator( + value: controller.progress / 100, + valueColor: AlwaysStoppedAnimation( + controller.isRunning.value ? Colors.green : Colors.grey, + ), + ), + ), + ), + const SizedBox(height: 24), ListenableBuilder( listenable: controller, - builder: (context, _) => Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, + builder: (context, _) => Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceEvenly, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 16, + runAlignment: WrapAlignment.spaceEvenly, + verticalDirection: VerticalDirection.down, + runSpacing: 16, children: [ - ValueListenableBuilder( - valueListenable: controller.isRunning, - builder: (context, running, child) => IconButton( - iconSize: 64, - icon: Icon(running ? Icons.timer : Icons.play_arrow, - color: running ? Colors.grey : Colors.red), - onPressed: running - ? null - : () { - final messenger = - ScaffoldMessenger.maybeOf(context); - controller.start( - onError: (error) => messenger - ?..clearSnackBars() - ..showSnackBar( - SnackBar( - content: Text('$error'), - backgroundColor: Colors.red, - ), - ), - ); - }, + SizedBox.square( + dimension: 128, + child: Center( + child: ValueListenableBuilder( + valueListenable: controller.isRunning, + builder: (context, running, child) => IconButton( + iconSize: 92, + tooltip: 'Start benchmark', + icon: Icon(running ? Icons.timer : Icons.play_arrow, + color: running ? Colors.grey : Colors.green), + onPressed: running + ? null + : () { + final messenger = + ScaffoldMessenger.maybeOf(context); + controller.start( + onError: (error) => messenger + ?..clearSnackBars() + ..showSnackBar( + SnackBar( + content: Text('$error'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 5), + ), + ), + ); + }, + ), + ), ), ), - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Pending: ${controller.pending}'), - Text('Sent: ${controller.sent}'), - Text('Received: ${controller.received}'), - Text('Failed: ${controller.failed}'), - Text('Total: ${controller.total}'), - Text('Progress: ${controller.progress}%'), - Text('Duration: ${controller.duration}ms'), - Text('Status: ${controller.status}'), - ], + SizedBox( + width: 256, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Pending: ${controller.pending}'), + Text('Sent: ${controller.sent} ' + '(${_formatBytes(controller.sentBytes)})'), + Text('Received: ${controller.received} ' + '(${_formatBytes(controller.receivedBytes)})'), + Text('Failed: ${controller.failed}'), + Text('Total: ${controller.total}'), + Text('Progress: ${controller.progress}%'), + Text('Duration: ${_formatMs(controller.duration)}'), + Text('Speed: ${controller.messagePerSecond} msg/s ' + '(${_formatBytes(controller.bytesPerSecond)}/s)'), + Text('Status: ${controller.status}'), + ], + ), ), ], ), ), - const Spacer(), ], ); } diff --git a/benchmark/lib/src/constant.dart b/benchmark/lib/src/constant.dart new file mode 100644 index 0000000..de0d1f2 --- /dev/null +++ b/benchmark/lib/src/constant.dart @@ -0,0 +1,5 @@ +/// HMAC key for the JWT token. +const String tokenHmacSecretKey = '80e88856-fe08-4a01-b9fc-73d1d03c2eee'; + +/// Default endpoint for the WebSocket connection. +const String defaultEndpoint = 'ws://localhost:8000/connection/websocket'; From 76d7d22ef0689f1ab67f3e18c6393d77a0b24d11 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 17 Aug 2024 14:34:11 +0400 Subject: [PATCH 29/40] refactor: Add HelpTab to benchmark app UI This commit adds the HelpTab component to the benchmark app UI. The HelpTab provides helpful information and instructions for using the app, including steps for setting up the benchmark environment and running the benchmark. This addition enhances the usability and user experience of the benchmark app. --- benchmark/lib/src/benchmark_app.dart | 5 +- benchmark/lib/src/benchmark_controller.dart | 2 +- benchmark/lib/src/help_tab.dart | 194 ++++++++++++++++++++ 3 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 benchmark/lib/src/help_tab.dart diff --git a/benchmark/lib/src/benchmark_app.dart b/benchmark/lib/src/benchmark_app.dart index 5a7abd7..46c9a7a 100644 --- a/benchmark/lib/src/benchmark_app.dart +++ b/benchmark/lib/src/benchmark_app.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:spinifybenchmark/src/benchmark_controller.dart'; import 'package:spinifybenchmark/src/benchmark_tab.dart'; +import 'package:spinifybenchmark/src/help_tab.dart'; class BenchmarkApp extends StatefulWidget { const BenchmarkApp({super.key}); @@ -135,9 +136,7 @@ class _BenchmarkScaffoldState extends State<_BenchmarkScaffold> /* Center( child: Text('Unknown'), ), */ - const Center( - child: Text('Help'), - ), + const HelpTab(), ], ), ), diff --git a/benchmark/lib/src/benchmark_controller.dart b/benchmark/lib/src/benchmark_controller.dart index e2fd81d..6b54c07 100644 --- a/benchmark/lib/src/benchmark_controller.dart +++ b/benchmark/lib/src/benchmark_controller.dart @@ -85,7 +85,7 @@ abstract base class BenchmarkControllerBase TextEditingController(text: defaultEndpoint); @override - final ValueNotifier payloadSize = ValueNotifier(255); + final ValueNotifier payloadSize = ValueNotifier(255 * 25); @override final ValueNotifier messageCount = ValueNotifier(1000); diff --git a/benchmark/lib/src/help_tab.dart b/benchmark/lib/src/help_tab.dart new file mode 100644 index 0000000..f73d570 --- /dev/null +++ b/benchmark/lib/src/help_tab.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class HelpTab extends StatelessWidget { + const HelpTab({ + super.key, // ignore: unused_element + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SelectionArea( + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + _HelpStep(1, [ + const TextSpan(text: 'Create a file named "'), + TextSpan( + text: 'docker-compose.yml', + style: TextStyle( + color: theme.colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + const TextSpan(text: '" in the project root directory.'), + ]), + const SizedBox(height: 16), + const _HelpStep(2, [ + TextSpan(text: 'Add the following content: '), + ]), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Stack( + fit: StackFit.loose, + alignment: Alignment.topLeft, + children: [ + const Text(_helpComposeContent), + Align( + alignment: Alignment.topRight, + child: IconButton( + icon: const Icon(Icons.copy), + onPressed: () async { + final messenger = ScaffoldMessenger.maybeOf(context); + await Clipboard.setData( + const ClipboardData(text: _helpComposeContent)); + messenger + ?..clearSnackBars() + ..showSnackBar( + const SnackBar( + content: Text('Copied to clipboard.'), + duration: Duration(seconds: 3), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 16), + _HelpStep(3, [ + const TextSpan(text: 'Run "'), + TextSpan( + text: 'docker-compose up', + style: TextStyle( + color: theme.colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + const TextSpan(text: '" to start the services.'), + ]), + const Divider(height: 16 * 2), + const _HelpStep(4, [ + TextSpan(text: 'Select the library to benchmark.'), + ]), + const SizedBox(height: 16), + const _HelpStep(5, [ + TextSpan(text: 'Enter the endpoint URL.'), + ]), + const SizedBox(height: 16), + const _HelpStep(6, [ + TextSpan(text: 'Select the payload size.'), + ]), + const SizedBox(height: 16), + const _HelpStep(7, [ + TextSpan(text: 'Select the message count.'), + ]), + const SizedBox(height: 16), + const _HelpStep(8, [ + TextSpan(text: 'Press the "'), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + Icons.play_arrow, + size: 18, + color: Colors.green, + ), + ), + TextSpan(text: '" button.'), + ]), + ], + ), + ); + } +} + +class _HelpStep extends StatelessWidget { + const _HelpStep( + this.step, + this.description, { + super.key, // ignore: unused_element + }); + + final int step; + + final List description; + + static const String _$nbsp = '\u00A0'; + static String _stepToEmoji(int step) => switch (step) { + 0 => '0️⃣', + 1 => '1️⃣', + 2 => '2️⃣', + 3 => '3️⃣', + 4 => '4️⃣', + 5 => '5️⃣', + 6 => '6️⃣', + 7 => '7️⃣', + 8 => '8️⃣', + 9 => '9️⃣', + 10 => '🔟', + _ => step.toString(), + }; + + @override + Widget build(BuildContext context) => Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${_stepToEmoji(step)}${_$nbsp * 2}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ...description, + ], + ), + ); +} + +const String _helpComposeContent = ''' +version: "3.9" + +services: + centrifugo-benchmark: + container_name: centrifugo-benchmark + image: centrifugo/centrifugo:v5 + restart: unless-stopped + command: centrifugo + tty: true + ports: + - 8000:8000 + environment: + - "CENTRIFUGO_ADMIN=true" + - "CENTRIFUGO_TOKEN_HMAC_SECRET_KEY=80e88856-fe08-4a01-b9fc-73d1d03c2eee" + - "CENTRIFUGO_ADMIN_PASSWORD=6cec4cc2-960d-4e4a-b650-0cbd4bbf0530" + - "CENTRIFUGO_ADMIN_SECRET=70957aac-555b-4bce-b9b8-53ada3a8029e" + - "CENTRIFUGO_API_KEY=8aba9113-d67a-41c6-818a-27aaaaeb64e7" + - "CENTRIFUGO_ALLOWED_ORIGINS=*" + - "CENTRIFUGO_HEALTH=true" + - "CENTRIFUGO_HISTORY_SIZE=10" + - "CENTRIFUGO_HISTORY_TTL=300s" + - "CENTRIFUGO_FORCE_RECOVERY=true" + - "CENTRIFUGO_ALLOW_PUBLISH_FOR_CLIENT=true" + - "CENTRIFUGO_ALLOW_SUBSCRIBE_FOR_CLIENT=true" + - "CENTRIFUGO_ALLOW_SUBSCRIBE_FOR_ANONYMOUS=true" + - "CENTRIFUGO_ALLOW_PUBLISH_FOR_SUBSCRIBER=true" + - "CENTRIFUGO_ALLOW_PUBLISH_FOR_ANONYMOUS=true" + - "CENTRIFUGO_ALLOW_USER_LIMITED_CHANNELS=true" + - "CENTRIFUGO_LOG_LEVEL=debug" + healthcheck: + test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:8000/health"] + interval: 3s + timeout: 3s + retries: 3 + ulimits: + nofile: + soft: 65535 + hard: 65535'''; From 69dfccfa18c9dc92fbb62197c1786911dc71671a Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 17 Aug 2024 14:51:07 +0400 Subject: [PATCH 30/40] refactor: Update benchmark app UI and status handling --- benchmark/docker-compose.yml | 2 +- benchmark/lib/src/benchmark_controller.dart | 106 ++++++++++++++++- benchmark/lib/src/benchmark_tab.dart | 122 ++++++++++++++++---- benchmark/lib/src/help_tab.dart | 2 +- 4 files changed, 207 insertions(+), 25 deletions(-) diff --git a/benchmark/docker-compose.yml b/benchmark/docker-compose.yml index 04b4e63..07111a4 100644 --- a/benchmark/docker-compose.yml +++ b/benchmark/docker-compose.yml @@ -9,7 +9,7 @@ services: container_name: centrifugo-benchmark image: centrifugo/centrifugo:v5 restart: unless-stopped - command: centrifugo + command: centrifugo --client_insecure --admin tty: true ports: - 8000:8000 diff --git a/benchmark/lib/src/benchmark_controller.dart b/benchmark/lib/src/benchmark_controller.dart index 6b54c07..5f5807e 100644 --- a/benchmark/lib/src/benchmark_controller.dart +++ b/benchmark/lib/src/benchmark_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:centrifuge/centrifuge.dart' as centrifuge_dart; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:spinify/spinify.dart'; @@ -141,6 +142,20 @@ abstract base class BenchmarkControllerBase int get bytesPerSecond => _bytesPerSecond; int _bytesPerSecond = 0; + void clear() { + _status = ''; + _pending = 0; + _sent = 0; + _sentBytes = 0; + _received = 0; + _receivedBytes = 0; + _failed = 0; + _total = 0; + _duration = 0; + _messagePerSecond = 0; + _bytesPerSecond = 0; + } + @override void dispose() { endpoint.dispose(); @@ -150,7 +165,8 @@ abstract base class BenchmarkControllerBase base mixin SpinifyBenchmark on BenchmarkControllerBase { Future startSpinify({void Function(Object error)? onError}) async { - // 65510 bytes + clear(); + // 65510 bytes is the maximum payload size for centrifugo. final payload = List.generate(payloadSize.value, (index) => index % 256); _duration = 0; @@ -182,7 +198,6 @@ base mixin SpinifyBenchmark on BenchmarkControllerBase { StreamSubscription? streamSubscription; Completer? completer; try { - _pending = _sent = _received = _failed = _duration = 0; pump('Subscribing to channel "benchmark"'); subscription = client.newSubscription('benchmark#1'); await subscription.subscribe(); @@ -238,7 +253,92 @@ base mixin SpinifyBenchmark on BenchmarkControllerBase { } base mixin CentrifugeBenchmark on BenchmarkControllerBase { - Future startCentrifuge({void Function(Object error)? onError}) async {} + Future startCentrifuge({void Function(Object error)? onError}) async { + clear(); + // 65510 bytes is the maximum payload size for centrifugo. + final payload = + List.generate(payloadSize.value, (index) => index % 256); + _duration = 0; + isRunning.value = true; + final stopwatch = Stopwatch()..start(); + void pump(String message) { + _status = message; + _duration = stopwatch.elapsedMilliseconds; + notifyListeners(); + } + + final centrifuge_dart.Client client; + try { + pump('Connecting to centrifugo'); + client = centrifuge_dart.createClient(endpoint.text, + centrifuge_dart.ClientConfig(getToken: (event) => _getToken())); + await client.connect(); + if (client.state != centrifuge_dart.State.connected) + throw Exception('Failed to connect'); + pump('Connected to ${endpoint.text}'); + } on Object catch (e) { + pump('Failed to connect'); + onError?.call(e); + stopwatch.stop(); + isRunning.value = false; + return; + } + + _total = messageCount.value; + centrifuge_dart.Subscription subscription; + StreamSubscription? streamSubscription; + Completer? completer; + try { + pump('Subscribing to channel "benchmark"'); + subscription = client.newSubscription('benchmark#1'); + await subscription.subscribe(); + streamSubscription = subscription.publication.listen((event) { + if (event.data.length == payload.length) { + _received++; + _receivedBytes += payload.length; + } else { + _failed++; + } + _duration = stopwatch.elapsedMilliseconds; + completer?.complete(); + }); + for (var i = 0; i < _total; i++) { + try { + _pending++; + pump('Sending message $i'); + completer = Completer(); + await subscription.publish(payload); + _sent++; + _sentBytes += payload.length; + pump('Sent message $i'); + await completer.future.timeout(const Duration(seconds: 5)); + pump('Received message $i'); + } on Object catch (e) { + _failed++; + onError?.call(e); + pump('Failed to send message $i'); + } finally { + _pending--; + if (stopwatch.elapsed.inMilliseconds case int ms when ms > 0) { + _messagePerSecond = (_sent + _received) * 1000 ~/ ms; + _bytesPerSecond = (_sentBytes + _receivedBytes) * 1000 ~/ ms; + } + } + } + pump('Unsubscribing from channel "benchmark"'); + await subscription.unsubscribe(); + pump('Done'); + } on Object catch (e) { + onError?.call(e); + pump('Failed. $e'); + return; + } finally { + stopwatch.stop(); + isRunning.value = false; + streamSubscription?.cancel().ignore(); + client.disconnect().ignore(); + } + } } final class BenchmarkControllerImpl extends BenchmarkControllerBase diff --git a/benchmark/lib/src/benchmark_tab.dart b/benchmark/lib/src/benchmark_tab.dart index 66754bb..d15d4e2 100644 --- a/benchmark/lib/src/benchmark_tab.dart +++ b/benchmark/lib/src/benchmark_tab.dart @@ -233,26 +233,108 @@ class BenchmarkTab extends StatelessWidget { ), ), ), - SizedBox( - width: 256, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Pending: ${controller.pending}'), - Text('Sent: ${controller.sent} ' - '(${_formatBytes(controller.sentBytes)})'), - Text('Received: ${controller.received} ' - '(${_formatBytes(controller.receivedBytes)})'), - Text('Failed: ${controller.failed}'), - Text('Total: ${controller.total}'), - Text('Progress: ${controller.progress}%'), - Text('Duration: ${_formatMs(controller.duration)}'), - Text('Speed: ${controller.messagePerSecond} msg/s ' - '(${_formatBytes(controller.bytesPerSecond)}/s)'), - Text('Status: ${controller.status}'), - ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: SizedBox( + width: 512, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton( + icon: const Icon(Icons.copy), + onPressed: () async { + final messenger = + ScaffoldMessenger.maybeOf(context); + final buffer = StringBuffer() + ..writeln( + 'Library: ${controller.library.value.name}') + ..writeln('Sent: ${controller.sent} ' + '(${_formatBytes(controller.sentBytes)})') + ..writeln('Received: ${controller.received} ' + '(${_formatBytes(controller.receivedBytes)})') + ..writeln('Failed: ${controller.failed}') + ..writeln('Total: ${controller.total}') + ..writeln('Progress: ${controller.progress}%') + ..writeln( + 'Duration: ${_formatMs(controller.duration)}') + ..writeln( + 'Speed: ${controller.messagePerSecond} msg/s ' + '(${_formatBytes(controller.bytesPerSecond)}/s)'); + + await Clipboard.setData( + ClipboardData(text: buffer.toString())); + messenger + ?..clearSnackBars() + ..showSnackBar( + const SnackBar( + content: Text('Copied to clipboard.'), + duration: Duration(seconds: 3), + ), + ); + }, + ), + const SizedBox(width: 8), + SelectionArea( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pending: ${controller.pending}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + 'Sent: ${controller.sent} ' + '(${_formatBytes(controller.sentBytes)})', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + 'Received: ${controller.received} ' + '(${_formatBytes(controller.receivedBytes)})', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + 'Failed: ${controller.failed}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + 'Total: ${controller.total}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + 'Progress: ${controller.progress}%', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + 'Duration: ${_formatMs(controller.duration)}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + 'Speed: ${controller.messagePerSecond} msg/s ' + '(${_formatBytes(controller.bytesPerSecond)}/s)', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + 'Status: ${controller.status}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), ), ), ], diff --git a/benchmark/lib/src/help_tab.dart b/benchmark/lib/src/help_tab.dart index f73d570..4ce180a 100644 --- a/benchmark/lib/src/help_tab.dart +++ b/benchmark/lib/src/help_tab.dart @@ -161,7 +161,7 @@ services: container_name: centrifugo-benchmark image: centrifugo/centrifugo:v5 restart: unless-stopped - command: centrifugo + command: centrifugo --client_insecure --admin tty: true ports: - 8000:8000 From 0345ac30164963b4010b3a80377bd1152292c7cc Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 17 Aug 2024 15:10:05 +0400 Subject: [PATCH 31/40] refactor: Update launch configuration for benchmark in VSCode --- .vscode/launch.json | 17 ++++++- benchmark/lib/main.dart | 2 +- benchmark/lib/src/benchmark_tab.dart | 72 ++++++++++++++++++++++------ 3 files changed, 74 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d601280..53a3fc0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "[Flutter] Benchmark", + "name": "[Flutter] Benchmark (debug)", "request": "launch", "type": "dart", "flutterMode": "debug", @@ -16,6 +16,21 @@ "toolArgs": [], "args": [] }, + { + "name": "[Flutter] Benchmark (release)", + "request": "launch", + "type": "dart", + "flutterMode": "release", + "cwd": "${workspaceFolder}/benchmark", + "program": "lib/main.dart", + "env": { + "ENVIRONMENT": "local" + }, + "console": "debugConsole", + "runTestsOnDevice": false, + "toolArgs": [], + "args": [] + }, /* { "name": "[Flutter] Example (Development)", "request": "launch", diff --git a/benchmark/lib/main.dart b/benchmark/lib/main.dart index ca2014b..47bda57 100644 --- a/benchmark/lib/main.dart +++ b/benchmark/lib/main.dart @@ -17,7 +17,7 @@ void _appZone(FutureOr Function() fn) => l.capture( const LogOptions( handlePrint: true, messageFormatting: _messageFormatting, - outputInRelease: false, + outputInRelease: true, printColors: true, ), ); diff --git a/benchmark/lib/src/benchmark_tab.dart b/benchmark/lib/src/benchmark_tab.dart index d15d4e2..b028611 100644 --- a/benchmark/lib/src/benchmark_tab.dart +++ b/benchmark/lib/src/benchmark_tab.dart @@ -99,14 +99,11 @@ class BenchmarkTab extends StatelessWidget { ); }, ), - Flexible( - fit: FlexFit.tight, - child: Text( - 'Token HMAC Secret Key:', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelLarge, - ), + Text( + 'Token HMAC Secret Key:', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelLarge, ), SelectableText( tokenHmacSecretKey, @@ -180,13 +177,11 @@ class BenchmarkTab extends StatelessWidget { const Divider(height: 48), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), - child: ListenableBuilder( - listenable: controller, - builder: (context, _) => LinearProgressIndicator( - value: controller.progress / 100, - valueColor: AlwaysStoppedAnimation( - controller.isRunning.value ? Colors.green : Colors.grey, - ), + child: SizedBox( + height: 24, + child: CustomPaint( + painter: ProgressPainter(controller), + child: const SizedBox(height: 8), ), ), ), @@ -343,3 +338,50 @@ class BenchmarkTab extends StatelessWidget { ], ); } + +class ProgressPainter extends CustomPainter { + const ProgressPainter(this.controller) : super(repaint: controller); + + final IBenchmarkController controller; + + static final backgroundPaint = Paint() + ..color = Colors.grey + ..strokeWidth = 8 + ..strokeCap = StrokeCap.round; + + static final progressPaint = Paint() + ..color = Colors.green + ..strokeWidth = 8 + ..strokeCap = StrokeCap.round; + + @override + void paint(Canvas canvas, Size size) { + canvas.drawRRect( + RRect.fromLTRBR( + 0, + 0, + size.width, + size.height, + const Radius.circular(8), + ), + backgroundPaint, + ); + if (controller.isRunning.value) + canvas.drawRRect( + RRect.fromLTRBR( + 0, + 0, + controller.progress / 100 * size.width, + size.height, + const Radius.circular(8), + ), + progressPaint, + ); + } + + @override + bool shouldRepaint(ProgressPainter oldDelegate) => false; + + @override + bool shouldRebuildSemantics(ProgressPainter oldDelegate) => false; +} From 25e9ecb932d78b3e384a4185095070bb039f59a5 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 17 Aug 2024 15:17:58 +0400 Subject: [PATCH 32/40] refactor: Update benchmark app UI and status handling --- .../.firebase/hosting.YnVpbGRcd2Vi.cache | 30 +++++++++++++++++++ benchmark/.firebaserc | 5 ++++ benchmark/README.md | 21 ++++++------- benchmark/firebase.json | 10 +++++++ 4 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 benchmark/.firebase/hosting.YnVpbGRcd2Vi.cache create mode 100644 benchmark/.firebaserc create mode 100644 benchmark/firebase.json diff --git a/benchmark/.firebase/hosting.YnVpbGRcd2Vi.cache b/benchmark/.firebase/hosting.YnVpbGRcd2Vi.cache new file mode 100644 index 0000000..766faf3 --- /dev/null +++ b/benchmark/.firebase/hosting.YnVpbGRcd2Vi.cache @@ -0,0 +1,30 @@ +favicon.png,1722443546310,0cab6e3dd5a9f008afdd133e1e1207cf65f2f2a10eb6712e3c209d8a5f76425a +index.html,1723893245262,94c885ed58e35dab51653d37043b144129aa594e4cbc29d9b4ab6ea55605eac8 +flutter.js,1723056583211,e029031dd81fb1264d754b510867c5a50f180b1e472b3ceef6aa8e393f4c8747 +flutter_bootstrap.js,1723893245255,d7007ebd7cafb5b3fa89ff914e9e7ee847e54f9fe56d8e074d58228377873110 +manifest.json,1723816256709,a6bc70f476defb641b7d37622afdea62492ed7322bde5876272beaecabdc50ef +flutter_service_worker.js,1723893263810,d7a0037b5da7fb64badcd15ad7e97b385eb23b80e836a2ea26c4dc4bb39982e9 +version.json,1723893262798,5edb0c3b00c5f5f9ba513f00fe869345404df18c8eea9c480b4fb7b13234abb2 +assets/AssetManifest.bin.json,1723893262852,6763aaa55146ec9534c671aaf5a6db93265eaaa8082767ea02eb2ac9b09cbdaa +assets/AssetManifest.bin,1723893262852,93e425eb6f3c87a588cd708f748fc5bb80fa28bf34c6d814b2bd76daa6a6ed27 +assets/AssetManifest.json,1723893262852,4dc0ac6e0cd8a5aca4a340ed626f7a9410f9abf8c874ee5e6ace847171e71c7a +assets/FontManifest.json,1723893262853,638dde6f87e8796f3054f78065f73846fc5e170e081d2501d08e3ceaa300edb5 +assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1723893263622,b2d803893415a3fb984c0202f9746f5fc8a3129d9b0a9e3284df3b74d39f0a0b +assets/fonts/MaterialIcons-Regular.otf,1723893263622,ff62477796319410b1e6267e47a0f84e367945278502785d17e92989b7bd4d7b +assets/shaders/ink_sparkle.frag,1723893262925,70b54a0a60a1e50bfc473198225b376030380c7b9dddba1ea9ce5180d654451c +canvaskit/skwasm.worker.js,1723056583208,4d46196245dc222fe063d4554ec2477feb1034714b964029f7f3975a62bba92d +icons/Icon-192.png,1722443546310,eaf2464bfb1d192fdd192a616f7b858dee456d573c6ec619648a1dcf2bdddfa6 +icons/Icon-maskable-192.png,1722443664829,196ce9142a3442ab37ae90cd46c3389e4660400c859b81cbb0538a51b39752eb +icons/Icon-512.png,1722443546311,9cf4cd298ae95acc1f25e97d88aa3f6bbfdf40867ea0f8a854c4393f49d56e64 +icons/Icon-maskable-512.png,1722443664828,6833b7c449e0dd24d5e164a53cc4557e643893e675b476b05efcbb9a6aa05bf0 +canvaskit/canvaskit.js,1723056583153,10625ba380417fde12bf47de2140331aa485c64b1634cc327110032ac8bb68e2 +canvaskit/skwasm.js,1723056583195,df56638e96c2f00fd95818a5a3068b71ed366864d8dd2c293ef269246c69eb74 +canvaskit/chromium/canvaskit.js,1723056583174,47ac4dc0b45535e29cbca024d3761a01c17158bf6ca6d62486dfc71bb1e2fe9d +assets/NOTICES,1723893262853,21a8ff1860c22dd7461706574f70a4abebbba381d13cfdce7936d6abb2b779a5 +canvaskit/chromium/canvaskit.js.symbols,1723056583176,a8976c3e515c86de158a328bede5c8a88f79e0bc12d30a85875e4ffb3f404251 +canvaskit/canvaskit.js.symbols,1723056583155,87c2807f94b798c5ac898c6fdc0932e323525e7cf8e704f26c6fe75220e88732 +canvaskit/skwasm.js.symbols,1723056583198,b3067b32d700a64ae05ac5e00a93e8fde95b2cdef4f5070e95d92155e0e2cf5d +main.dart.js,1723893262437,1e457ead6494a8549a24b2638e16efe381bc934346b1fb87178f9a87364fc9ab +canvaskit/skwasm.wasm,1723056583208,b897540aae7f275d8e4cfe1bcf3eed4275dd3482ac3d7fb643400234f9c5ebce +canvaskit/chromium/canvaskit.wasm,1723056583195,5bbcff71f1b43685c81e0dbdb238cfffac82116f63e1b05dd1c50ae4aa456bec +canvaskit/canvaskit.wasm,1723056583173,7aa921af181b1989bfe88875aa6eb8131e90f57d8e839ccd9a293ef39ed1dee0 diff --git a/benchmark/.firebaserc b/benchmark/.firebaserc new file mode 100644 index 0000000..1c2e59d --- /dev/null +++ b/benchmark/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "spinify-benchmark" + } +} diff --git a/benchmark/README.md b/benchmark/README.md index e17ed40..77910e9 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1,16 +1,13 @@ -# spinifybenchmark +# Spinify: benchmark -Benchmark +## Build -## Getting Started +```bash +flutter build web --release --no-source-maps --pwa-strategy offline-first --web-renderer canvaskit --web-resources-cdn --base-href / +``` -This project is a starting point for a Flutter application. +## Deploy -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +```bash +firebase deploy --only hosting +``` diff --git a/benchmark/firebase.json b/benchmark/firebase.json new file mode 100644 index 0000000..0d25a77 --- /dev/null +++ b/benchmark/firebase.json @@ -0,0 +1,10 @@ +{ + "hosting": { + "public": "build/web", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + } +} From 8e1dc51c14e2706481d910cd5a2e4f8a75c1dccb Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 17 Aug 2024 15:20:32 +0400 Subject: [PATCH 33/40] refactor: Update firebase.json hosting ignore rules This commit updates the `firebase.json` file to modify the hosting ignore rules. The previous rules were spread across multiple lines, which made the file harder to read and maintain. This change consolidates the ignore rules into a single line, improving the readability and maintainability of the file. --- benchmark/firebase.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/benchmark/firebase.json b/benchmark/firebase.json index 0d25a77..d02031b 100644 --- a/benchmark/firebase.json +++ b/benchmark/firebase.json @@ -1,10 +1,6 @@ { "hosting": { "public": "build/web", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ] + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"] } } From 8513b081a49142f3f206880000e67b44184aa90f Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 17 Aug 2024 15:37:40 +0400 Subject: [PATCH 34/40] refactor: Update app titles to include "Spinify" This commit updates the titles of the benchmark app to include "Spinify" for better branding consistency. The app titles were changed from "Benchmark" to "Spinify Benchmark" in both the main app and the app bar. This change improves the branding and recognition of the app. --- benchmark/lib/src/benchmark_app.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark/lib/src/benchmark_app.dart b/benchmark/lib/src/benchmark_app.dart index 46c9a7a..7555baa 100644 --- a/benchmark/lib/src/benchmark_app.dart +++ b/benchmark/lib/src/benchmark_app.dart @@ -39,7 +39,7 @@ class _BenchmarkAppState extends State { @override Widget build(BuildContext context) => MaterialApp( debugShowCheckedModeBanner: false, - title: 'Benchmark', + title: 'Spinify Benchmark', themeMode: themeMode.value, theme: switch (themeMode.value) { ThemeMode.dark => ThemeData.dark(), @@ -84,7 +84,7 @@ class _BenchmarkScaffoldState extends State<_BenchmarkScaffold> @override Widget build(BuildContext context) => Scaffold( appBar: AppBar( - title: const Text('Benchmark'), + title: const Text('Spinify Benchmark'), actions: [ ValueListenableBuilder( valueListenable: widget.themeMode, From 44f210c1e0d54fbae01160f2da9e6af64a80e04a Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 17 Aug 2024 16:03:13 +0400 Subject: [PATCH 35/40] refactor: Update app titles to include "Spinify" --- .gitignore | 5 +++- .../.firebase/hosting.YnVpbGRcd2Vi.cache | 30 ------------------- lib/src/transport_ws_pb_js.dart | 8 +++++ 3 files changed, 12 insertions(+), 31 deletions(-) delete mode 100644 benchmark/.firebase/hosting.YnVpbGRcd2Vi.cache diff --git a/.gitignore b/.gitignore index aded267..cdce770 100644 --- a/.gitignore +++ b/.gitignore @@ -86,4 +86,7 @@ config.json # Binaries for programs and plugins *.exe -*.exe~ \ No newline at end of file +*.exe~ + +# Firebase +.firebase/ \ No newline at end of file diff --git a/benchmark/.firebase/hosting.YnVpbGRcd2Vi.cache b/benchmark/.firebase/hosting.YnVpbGRcd2Vi.cache deleted file mode 100644 index 766faf3..0000000 --- a/benchmark/.firebase/hosting.YnVpbGRcd2Vi.cache +++ /dev/null @@ -1,30 +0,0 @@ -favicon.png,1722443546310,0cab6e3dd5a9f008afdd133e1e1207cf65f2f2a10eb6712e3c209d8a5f76425a -index.html,1723893245262,94c885ed58e35dab51653d37043b144129aa594e4cbc29d9b4ab6ea55605eac8 -flutter.js,1723056583211,e029031dd81fb1264d754b510867c5a50f180b1e472b3ceef6aa8e393f4c8747 -flutter_bootstrap.js,1723893245255,d7007ebd7cafb5b3fa89ff914e9e7ee847e54f9fe56d8e074d58228377873110 -manifest.json,1723816256709,a6bc70f476defb641b7d37622afdea62492ed7322bde5876272beaecabdc50ef -flutter_service_worker.js,1723893263810,d7a0037b5da7fb64badcd15ad7e97b385eb23b80e836a2ea26c4dc4bb39982e9 -version.json,1723893262798,5edb0c3b00c5f5f9ba513f00fe869345404df18c8eea9c480b4fb7b13234abb2 -assets/AssetManifest.bin.json,1723893262852,6763aaa55146ec9534c671aaf5a6db93265eaaa8082767ea02eb2ac9b09cbdaa -assets/AssetManifest.bin,1723893262852,93e425eb6f3c87a588cd708f748fc5bb80fa28bf34c6d814b2bd76daa6a6ed27 -assets/AssetManifest.json,1723893262852,4dc0ac6e0cd8a5aca4a340ed626f7a9410f9abf8c874ee5e6ace847171e71c7a -assets/FontManifest.json,1723893262853,638dde6f87e8796f3054f78065f73846fc5e170e081d2501d08e3ceaa300edb5 -assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1723893263622,b2d803893415a3fb984c0202f9746f5fc8a3129d9b0a9e3284df3b74d39f0a0b -assets/fonts/MaterialIcons-Regular.otf,1723893263622,ff62477796319410b1e6267e47a0f84e367945278502785d17e92989b7bd4d7b -assets/shaders/ink_sparkle.frag,1723893262925,70b54a0a60a1e50bfc473198225b376030380c7b9dddba1ea9ce5180d654451c -canvaskit/skwasm.worker.js,1723056583208,4d46196245dc222fe063d4554ec2477feb1034714b964029f7f3975a62bba92d -icons/Icon-192.png,1722443546310,eaf2464bfb1d192fdd192a616f7b858dee456d573c6ec619648a1dcf2bdddfa6 -icons/Icon-maskable-192.png,1722443664829,196ce9142a3442ab37ae90cd46c3389e4660400c859b81cbb0538a51b39752eb -icons/Icon-512.png,1722443546311,9cf4cd298ae95acc1f25e97d88aa3f6bbfdf40867ea0f8a854c4393f49d56e64 -icons/Icon-maskable-512.png,1722443664828,6833b7c449e0dd24d5e164a53cc4557e643893e675b476b05efcbb9a6aa05bf0 -canvaskit/canvaskit.js,1723056583153,10625ba380417fde12bf47de2140331aa485c64b1634cc327110032ac8bb68e2 -canvaskit/skwasm.js,1723056583195,df56638e96c2f00fd95818a5a3068b71ed366864d8dd2c293ef269246c69eb74 -canvaskit/chromium/canvaskit.js,1723056583174,47ac4dc0b45535e29cbca024d3761a01c17158bf6ca6d62486dfc71bb1e2fe9d -assets/NOTICES,1723893262853,21a8ff1860c22dd7461706574f70a4abebbba381d13cfdce7936d6abb2b779a5 -canvaskit/chromium/canvaskit.js.symbols,1723056583176,a8976c3e515c86de158a328bede5c8a88f79e0bc12d30a85875e4ffb3f404251 -canvaskit/canvaskit.js.symbols,1723056583155,87c2807f94b798c5ac898c6fdc0932e323525e7cf8e704f26c6fe75220e88732 -canvaskit/skwasm.js.symbols,1723056583198,b3067b32d700a64ae05ac5e00a93e8fde95b2cdef4f5070e95d92155e0e2cf5d -main.dart.js,1723893262437,1e457ead6494a8549a24b2638e16efe381bc934346b1fb87178f9a87364fc9ab -canvaskit/skwasm.wasm,1723056583208,b897540aae7f275d8e4cfe1bcf3eed4275dd3482ac3d7fb643400234f9c5ebce -canvaskit/chromium/canvaskit.wasm,1723056583195,5bbcff71f1b43685c81e0dbdb238cfffac82116f63e1b05dd1c50ae4aa456bec -canvaskit/canvaskit.wasm,1723056583173,7aa921af181b1989bfe88875aa6eb8131e90f57d8e839ccd9a293ef39ed1dee0 diff --git a/lib/src/transport_ws_pb_js.dart b/lib/src/transport_ws_pb_js.dart index 3107cb5..64b08e3 100644 --- a/lib/src/transport_ws_pb_js.dart +++ b/lib/src/transport_ws_pb_js.dart @@ -137,6 +137,14 @@ Future $create$WS$PB$Transport({ }); }); + /* socket.onmessage = (web.MessageEvent event) { + final data = event.data; + eventQueue.add(() async { + if (transport == null || transport.disconnected) return; + final bytes = await _blobCodec.read(data); + transport._onData(bytes); + }); + }.toJS; */ onMessage = socket.onMessage.listen((event) { eventQueue.add(() async { final bytes = await _blobCodec.read(event.data); From 468cd0ca490f323adaa5f2f339271a4318bcfc08 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sat, 17 Aug 2024 16:04:43 +0400 Subject: [PATCH 36/40] refactor: Update timestamp in pubspec.yaml.g.dart This commit updates the timestamp in the pubspec.yaml.g.dart file to reflect the correct build date and time. The previous values were incorrect and have been corrected to ensure accurate tracking of build information. This change improves the reliability and integrity of the build process. --- lib/src/model/pubspec.yaml.g.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/model/pubspec.yaml.g.dart b/lib/src/model/pubspec.yaml.g.dart index c7b9abb..c47cd68 100644 --- a/lib/src/model/pubspec.yaml.g.dart +++ b/lib/src/model/pubspec.yaml.g.dart @@ -124,13 +124,13 @@ sealed class Pubspec { /// Build date and time (UTC) static final DateTime timestamp = DateTime.utc( 2024, - 7, + 8, 17, - 22, - 36, - 15, - 282, - 791, + 12, + 4, + 14, + 247, + 353, ); /// Name From 01a691605bea22b8295c006f5fa4aac744306dc4 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sun, 18 Aug 2024 10:30:46 +0400 Subject: [PATCH 37/40] refactor: Update version to 0.0.2 in pubspec.yaml --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b8e21d..b85b300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.2 + +- Basic functionality implemented + ## 0.0.1-pre.9 - **ADDED**: Initial release diff --git a/pubspec.yaml b/pubspec.yaml index 48ff250..7917d16 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ description: > Dart client to communicate with Centrifuge and Centrifugo from Flutter and VM over WebSockets -version: 0.0.1-pre.9 +version: 0.0.2 homepage: https://centrifugal.dev From ce62318fc297385746c36a8115e87d3e6b517fa5 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sun, 18 Aug 2024 10:31:10 +0400 Subject: [PATCH 38/40] refactor: Update pubspec.yaml to add Protobuf support for WebSockets --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7917d16..e177e79 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: spinify description: > Dart client to communicate with Centrifuge and Centrifugo from Flutter and VM - over WebSockets + over WebSockets with Protobuf support. version: 0.0.2 From 023fdcef7721af1340080edb5610e650c1ce778e Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sun, 18 Aug 2024 10:31:31 +0400 Subject: [PATCH 39/40] refactor: Update description in pubspec.yaml for Dart and Flutter compatibility --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e177e79..fdb7b95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spinify description: > - Dart client to communicate with Centrifuge and Centrifugo from Flutter and VM + Dart client to communicate with Centrifuge and Centrifugo from Dart and Flutter over WebSockets with Protobuf support. version: 0.0.2 From 8d821a018a1fb3d859ef0118893c0a65600e1496 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sun, 18 Aug 2024 10:35:24 +0400 Subject: [PATCH 40/40] Update version --- .github/workflows/checkout.yml | 3 ++- lib/src/model/pubspec.yaml.g.dart | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/checkout.yml b/.github/workflows/checkout.yml index 0eae08f..0f69482 100644 --- a/.github/workflows/checkout.yml +++ b/.github/workflows/checkout.yml @@ -86,7 +86,7 @@ jobs: id: verify-versions timeout-minutes: 1 run: | - test -f pubspec.yaml && test -f lib/src/model/pubspec.yaml.g.dart + test -f pubspec.yaml && test -f lib/src/model/pubspec.yaml.g.dart && test -f CHANGELOG.md version_pubspec=$(grep '^version:' pubspec.yaml | awk '{print $2}' | sed 's/[^[:print:]]//g') version_dart=$(grep 'representation: r' lib/src/model/pubspec.yaml.g.dart | awk -F"'" '{print $2}' | sed 's/[^[:print:]]//g') test -n "$version_pubspec" && test -n "$version_dart" @@ -95,6 +95,7 @@ jobs: echo "$version_pubspec" > /tmp/version_pubspec echo "$version_dart" > /tmp/version_dart diff /tmp/version_pubspec /tmp/version_dart + grep -q "# $version_pubspec" CHANGELOG.md || (echo "Version not found in CHANGELOG.md" >&2; exit 1) - name: 🧪 Run unit tests id: run-unit-tests diff --git a/lib/src/model/pubspec.yaml.g.dart b/lib/src/model/pubspec.yaml.g.dart index c47cd68..148c1b3 100644 --- a/lib/src/model/pubspec.yaml.g.dart +++ b/lib/src/model/pubspec.yaml.g.dart @@ -93,13 +93,13 @@ sealed class Pubspec { static const PubspecVersion version = ( /// Non-canonical string representation of the version as provided /// in the pubspec.yaml file. - representation: r'0.0.1-pre.9', + representation: r'0.0.2', /// Returns a 'canonicalized' representation /// of the application version. /// This represents the version string in accordance with /// Semantic Versioning (SemVer) standards. - canonical: r'0.0.1-pre.9', + canonical: r'0.0.2', /// MAJOR version when you make incompatible API changes. /// The major version number: 1 in "1.2.3". @@ -112,10 +112,10 @@ sealed class Pubspec { /// PATCH version when you make backward compatible bug fixes. /// The patch version number: 3 in "1.2.3". - patch: 1, + patch: 2, /// The pre-release identifier: "foo" in "1.2.3-foo". - preRelease: [r'pre', r'9'], + preRelease: [], /// The build identifier: "foo" in "1.2.3+foo". build: [], @@ -125,12 +125,12 @@ sealed class Pubspec { static final DateTime timestamp = DateTime.utc( 2024, 8, - 17, - 12, - 4, - 14, - 247, - 353, + 18, + 6, + 33, + 36, + 43, + 85, ); /// Name @@ -166,7 +166,7 @@ sealed class Pubspec { /// Users see it when they [browse for packages](https://pub.dev/packages). /// The description is plain text: no markdown or HTML. static const String description = - r'Dart client to communicate with Centrifuge and Centrifugo from Flutter and VM over WebSockets'; + r'Dart client to communicate with Centrifuge and Centrifugo from Dart and Flutter over WebSockets with Protobuf support.'; /// Homepage ///