Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] create a dashboard for investigating React on Rails SSR performance #1636

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
react_on_rails (14.0.0)
react_on_rails (14.0.2)
addressable
connection_pool
execjs (~> 2.5)
Expand Down Expand Up @@ -119,6 +119,9 @@ GEM
tins (~> 1.6)
crass (1.0.6)
date (3.3.4)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
diff-lcs (1.5.1)
docile (1.4.0)
drb (2.2.1)
Expand Down Expand Up @@ -397,6 +400,7 @@ DEPENDENCIES
capybara
capybara-screenshot
coveralls
debug
equivalent-xml
gem-release
generator_spec
Expand Down
77 changes: 77 additions & 0 deletions app/controllers/trace_visualizer/trace_visualizer_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

module TraceVisualizer
class TraceVisualizerController < ActionController::Base
def index
log_file_path = "./log1.log"
log_file_content = File.read(log_file_path)

map_request_id_to_path = {}
map_request_id_to_operation_stack = {}

log_file_content.each_line do |line|
# get lines like this:
# [04b9a1be-1312-4053-9598-f500a81f0203] Started GET "/server_side_hello_world_hooks" for ::1 at 2024-06-24 12:27:08 +0300
# it is a request start line.
# Request id is between square brackets, request method is after "Started", request path is in quotes after method name.
if line =~ /\[(\h{8}-\h{4}-\h{4}-\h{4}-\h{12})\] Started (\w+) "(.*)" for/
request_id = ::Regexp.last_match(1)
path = ::Regexp.last_match(3)
map_request_id_to_path[request_id] = path
map_request_id_to_operation_stack[request_id] = []
end

# Each operation logs the following line to logs
# [04b9a1be-1312-4053-9598-f500a81f0203] [ReactOnRailsPro] [operation-start] PID:49996 server_rendering_component_js_code: HelloWorldHooks, 171923395.5230
# where last number is the timestamp of the operation start
# After finishing the operation it logs the following line
# [04b9a1be-1312-4053-9598-f500a81f0203] [ReactOnRailsPro] PID:49996 server_rendering_component_js_code: HelloWorldHooks, 2.1ms
# We need to extract the request id, operation name and duration of the operation
# Also, we need to extract suboperations
if line =~ /\[(\h{8}-\h{4}-\h{4}-\h{4}-\h{12})\] \[ReactOnRails\] \[operation-start\] PID:\d+ (\w+): (.*), (\d+\.\d+)/
request_id = ::Regexp.last_match(1)
operation_name = ::Regexp.last_match(2)
message = ::Regexp.last_match(3)
start_time = ::Regexp.last_match(4).to_f
map_request_id_to_operation_stack[request_id] << {
operation_name: operation_name,
message: message,
suboperations: [],
start_time: start_time,
}
end

next unless line =~ /\[(\h{8}-\h{4}-\h{4}-\h{4}-\h{12})\] \[ReactOnRails\] PID:\d+ (\w+): (.*), (\d+\.\d+)ms/

# binding.pry
request_id = ::Regexp.last_match(1)
operation_name = ::Regexp.last_match(2)
message = ::Regexp.last_match(3)
duration = ::Regexp.last_match(4).to_f
current_operation_in_stack = map_request_id_to_operation_stack[request_id].last

if current_operation_in_stack[:operation_name] != operation_name || current_operation_in_stack[:message] != message
raise "Unmatched operation name"
end

current_operation_in_stack[:duration] = duration
if map_request_id_to_operation_stack[request_id].size > 1
map_request_id_to_operation_stack[request_id].pop
map_request_id_to_operation_stack[request_id].last[:suboperations] << current_operation_in_stack
end
end

# render map_request_id_to_operation_stack to json
# replace request ids with paths
@json_data = map_request_id_to_operation_stack.map do |request_id, operation_stack|
path = map_request_id_to_path[request_id]
{ path: path, operation_stack: operation_stack }
end
@json_data = @json_data.to_json

# render the view in app/views/trace_visualizer/trace_visualizer/index.html.erb
# with the json data
render "index"
end
Comment on lines +5 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimize log file processing and error handling.

The index method effectively parses log files and maps operations. Consider handling potential errors such as file not found or unreadable logs to enhance robustness.

+      if !File.exist?(log_file_path)
+        raise "Log file not found at #{log_file_path}"
+      end
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def index
log_file_path = "./log1.log"
log_file_content = File.read(log_file_path)
map_request_id_to_path = {}
map_request_id_to_operation_stack = {}
log_file_content.each_line do |line|
# get lines like this:
# [04b9a1be-1312-4053-9598-f500a81f0203] Started GET "/server_side_hello_world_hooks" for ::1 at 2024-06-24 12:27:08 +0300
# it is a request start line.
# Request id is between square brackets, request method is after "Started", request path is in quotes after method name.
if line =~ /\[(\h{8}-\h{4}-\h{4}-\h{4}-\h{12})\] Started (\w+) "(.*)" for/
request_id = ::Regexp.last_match(1)
path = ::Regexp.last_match(3)
map_request_id_to_path[request_id] = path
map_request_id_to_operation_stack[request_id] = []
end
# Each operation logs the following line to logs
# [04b9a1be-1312-4053-9598-f500a81f0203] [ReactOnRailsPro] [operation-start] PID:49996 server_rendering_component_js_code: HelloWorldHooks, 171923395.5230
# where last number is the timestamp of the operation start
# After finishing the operation it logs the following line
# [04b9a1be-1312-4053-9598-f500a81f0203] [ReactOnRailsPro] PID:49996 server_rendering_component_js_code: HelloWorldHooks, 2.1ms
# We need to extract the request id, operation name and duration of the operation
# Also, we need to extract suboperations
if line =~ /\[(\h{8}-\h{4}-\h{4}-\h{4}-\h{12})\] \[ReactOnRails\] \[operation-start\] PID:\d+ (\w+): (.*), (\d+\.\d+)/
request_id = ::Regexp.last_match(1)
operation_name = ::Regexp.last_match(2)
message = ::Regexp.last_match(3)
start_time = ::Regexp.last_match(4).to_f
map_request_id_to_operation_stack[request_id] << {
operation_name: operation_name,
message: message,
suboperations: [],
start_time: start_time,
}
end
next unless line =~ /\[(\h{8}-\h{4}-\h{4}-\h{4}-\h{12})\] \[ReactOnRails\] PID:\d+ (\w+): (.*), (\d+\.\d+)ms/
# binding.pry
request_id = ::Regexp.last_match(1)
operation_name = ::Regexp.last_match(2)
message = ::Regexp.last_match(3)
duration = ::Regexp.last_match(4).to_f
current_operation_in_stack = map_request_id_to_operation_stack[request_id].last
if current_operation_in_stack[:operation_name] != operation_name || current_operation_in_stack[:message] != message
raise "Unmatched operation name"
end
current_operation_in_stack[:duration] = duration
if map_request_id_to_operation_stack[request_id].size > 1
map_request_id_to_operation_stack[request_id].pop
map_request_id_to_operation_stack[request_id].last[:suboperations] << current_operation_in_stack
end
end
# render map_request_id_to_operation_stack to json
# replace request ids with paths
@json_data = map_request_id_to_operation_stack.map do |request_id, operation_stack|
path = map_request_id_to_path[request_id]
{ path: path, operation_stack: operation_stack }
end
@json_data = @json_data.to_json
# render the view in app/views/trace_visualizer/trace_visualizer/index.html.erb
# with the json data
render "index"
end
def index
log_file_path = "./log1.log"
if !File.exist?(log_file_path)
raise "Log file not found at #{log_file_path}"
end
log_file_content = File.read(log_file_path)
map_request_id_to_path = {}
map_request_id_to_operation_stack = {}
log_file_content.each_line do |line|
# get lines like this:
# [04b9a1be-1312-4053-9598-f500a81f0203] Started GET "/server_side_hello_world_hooks" for ::1 at 2024-06-24 12:27:08 +0300
# it is a request start line.
# Request id is between square brackets, request method is after "Started", request path is in quotes after method name.
if line =~ /\[(\h{8}-\h{4}-\h{4}-\h{4}-\h{12})\] Started (\w+) "(.*)" for/
request_id = ::Regexp.last_match(1)
path = ::Regexp.last_match(3)
map_request_id_to_path[request_id] = path
map_request_id_to_operation_stack[request_id] = []
end
# Each operation logs the following line to logs
# [04b9a1be-1312-4053-9598-f500a81f0203] [ReactOnRailsPro] [operation-start] PID:49996 server_rendering_component_js_code: HelloWorldHooks, 171923395.5230
# where last number is the timestamp of the operation start
# After finishing the operation it logs the following line
# [04b9a1be-1312-4053-9598-f500a81f0203] [ReactOnRailsPro] PID:49996 server_rendering_component_js_code: HelloWorldHooks, 2.1ms
# We need to extract the request id, operation name and duration of the operation
# Also, we need to extract suboperations
if line =~ /\[(\h{8}-\h{4}-\h{4}-\h{4}-\h{12})\] \[ReactOnRails\] \[operation-start\] PID:\d+ (\w+): (.*), (\d+\.\d+)/
request_id = ::Regexp.last_match(1)
operation_name = ::Regexp.last_match(2)
message = ::Regexp.last_match(3)
start_time = ::Regexp.last_match(4).to_f
map_request_id_to_operation_stack[request_id] << {
operation_name: operation_name,
message: message,
suboperations: [],
start_time: start_time,
}
end
next unless line =~ /\[(\h{8}-\h{4}-\h{4}-\h{4}-\h{12})\] \[ReactOnRails\] PID:\d+ (\w+): (.*), (\d+\.\d+)ms/
# binding.pry
request_id = ::Regexp.last_match(1)
operation_name = ::Regexp.last_match(2)
message = ::Regexp.last_match(3)
duration = ::Regexp.last_match(4).to_f
current_operation_in_stack = map_request_id_to_operation_stack[request_id].last
if current_operation_in_stack[:operation_name] != operation_name || current_operation_in_stack[:message] != message
raise "Unmatched operation name"
end
current_operation_in_stack[:duration] = duration
if map_request_id_to_operation_stack[request_id].size > 1
map_request_id_to_operation_stack[request_id].pop
map_request_id_to_operation_stack[request_id].last[:suboperations] << current_operation_in_stack
end
end
# render map_request_id_to_operation_stack to json
# replace request ids with paths
@json_data = map_request_id_to_operation_stack.map do |request_id, operation_stack|
path = map_request_id_to_path[request_id]
{ path: path, operation_stack: operation_stack }
end
@json_data = @json_data.to_json
# render the view in app/views/trace_visualizer/trace_visualizer/index.html.erb
# with the json data
render "index"
end

end
end
108 changes: 108 additions & 0 deletions app/views/trace_visualizer/trace_visualizer/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React on Rails Performance Report</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
}
.gantt-chart {
width: 100%;
position: relative;
height: auto;
border-left: 2px solid #000;
border-top: 1px solid #000;
}
.task {
position: absolute;
height: 30px;
background-color: #76A5AF;
color: white;
line-height: 30px;
padding-left: 5px;
border-radius: 5px;
border: 1px solid #639;
}
</style>
</head>
<body>
<div id="chartContainer" class="gantt-chart"></div>

<script>
window.OPERATIONS_TIMELINE = <%= raw @json_data %>
</script>

<script>
// Sample JSON data
const operations = window.OPERATIONS_TIMELINE;

function findMinStartTime(requestData) {
let minStartTime = Infinity;
requestData.operation_stack.forEach(op => {
minStartTime = Math.min(minStartTime, op.start_time);
op.suboperations.forEach(subOp => {
minStartTime = Math.min(minStartTime, subOp.start_time);
});
});
return minStartTime;
}

function renderGanttChart(requestData, requestLevel) {
const minStartTime = findMinStartTime(requestData);
console.log(`MIN Start Time: ${minStartTime}`);
let maxEndTime = 0;
requestData.operation_stack.forEach((op, stackIndex) => {
const startTime = (op.start_time - minStartTime);
const duration = op.duration;
maxEndTime = Math.max(maxEndTime, startTime + duration);

op.suboperations.forEach(subOp => {
const subStartTime = (subOp.start_time - minStartTime);
const subDuration = subOp.duration;
maxEndTime = Math.max(maxEndTime, subStartTime + subDuration);
});
});

const scale = 1000 / maxEndTime; // Scaling to fit in 1000px width
function renderOperation(op, startTime, top) {
const duration = op.duration * scale;
const task = document.createElement('div');
task.className = 'task';
task.style.top = `${top}px`;
task.style.left = `${startTime*1000}px`;
task.style.width = `${duration}px`;
task.textContent = `${op.operation_name} (${op.duration.toFixed(2)} ms)`;
// add tooltip
task.title = `${op.operation_name}-${op.message} (${op.duration.toFixed(2)} ms)`;
chartContainer.appendChild(task);

let upperLevels = 0;
op.suboperations.forEach((subOp, subIndex) => {
const subStartTime = (subOp.start_time - minStartTime) * scale;
const upperBranchLevel = renderOperation(subOp, subStartTime, top + 40);
upperLevels = upperBranchLevel > upperLevels ? upperBranchLevel : upperLevels;
});
return upperLevels + 1;
}

let levels = requestLevel;
requestData.operation_stack.forEach((op) => {
const startTime = (op.start_time - minStartTime) * scale;
levels += renderOperation(op, startTime, levels * 40); // Increment top value for each operation, not suboperation
});
levels++;
return levels;
}

let requestLevels = 0;
operations.forEach(requestData => {
const currentRequestLevels = renderGanttChart(requestData, requestLevels)
requestLevels += currentRequestLevels;
});
chartContainer.height = `${requestLevels * 40 + 80}px`
</script>
</body>
</html>
Comment on lines +1 to +108
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comprehensive Review of the Trace Visualizer HTML and JavaScript

The overall structure and implementation of the HTML and JavaScript for the Trace Visualizer is robust and well-organized. The use of inline JavaScript to manipulate DOM elements for dynamic Gantt chart rendering is appropriate given the context of a performance visualization tool. The CSS styling is minimalistic yet effective, ensuring that the visual elements are clear and concise.

However, there are a few areas that could be improved:

  1. JavaScript Modularity: The JavaScript code is embedded directly within the HTML document. For better maintainability and scalability, consider extracting this into separate JavaScript files. This not only cleans up the HTML file but also allows for easier testing and reusability of the JavaScript code.
  2. Error Handling in JavaScript: While the script handles the basic flow well, adding error handling around JSON parsing and DOM manipulations could prevent runtime errors and improve the robustness of the page.
  3. Accessibility: Ensure that the dynamically generated DOM elements (tasks in the Gantt chart) are accessible. This includes proper roles, labels, and keyboard navigability.
  4. Performance Considerations: For large datasets, the current implementation might become slow as it involves multiple nested loops and DOM manipulations. Consider optimizing the rendering logic or using a virtual DOM approach to handle larger datasets efficiently.

Overall, the implementation meets the basic requirements but could be enhanced by addressing the above points.

5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

TraceVisualizer::Engine.routes.draw do
get "/", to: "trace_visualizer#index"
end
1 change: 1 addition & 0 deletions lib/react_on_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@
require "react_on_rails/locales/base"
require "react_on_rails/locales/to_js"
require "react_on_rails/locales/to_json"
require 'trace_visualizer/engine'
42 changes: 22 additions & 20 deletions lib/react_on_rails/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -431,25 +431,27 @@ def internal_react_component(react_component_name, options = {})
render_options = ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name,
options: options)

# Setup the page_loaded_js, which is the same regardless of prerendering or not!
# The reason is that React is smart about not doing extra work if the server rendering did its job.
component_specification_tag = content_tag(:script,
json_safe_and_pretty(render_options.client_props).html_safe,
type: "application/json",
class: "js-react-on-rails-component",
"data-component-name" => render_options.react_component_name,
"data-trace" => (render_options.trace ? true : nil),
"data-dom-id" => render_options.dom_id)

load_pack_for_generated_component(react_component_name, render_options)
# Create the HTML rendering part
result = server_rendered_react_component(render_options)

{
render_options: render_options,
tag: component_specification_tag,
result: result
}
ReactOnRails::Utils.with_trace "#{react_component_name}: Full rendering process" do
# Setup the page_loaded_js, which is the same regardless of prerendering or not!
# The reason is that React is smart about not doing extra work if the server rendering did its job.
component_specification_tag = content_tag(:script,
json_safe_and_pretty(render_options.client_props).html_safe,
type: "application/json",
class: "js-react-on-rails-component",
"data-component-name" => render_options.react_component_name,
"data-trace" => (render_options.trace ? true : nil),
"data-dom-id" => render_options.dom_id)

load_pack_for_generated_component(react_component_name, render_options)
# Create the HTML rendering part
result = server_rendered_react_component(render_options)

{
render_options: render_options,
tag: component_specification_tag,
result: result
}
end
end

def render_redux_store_data(redux_store_data)
Expand Down Expand Up @@ -477,7 +479,7 @@ def server_rendered_react_component(render_options)

# Make sure that we use up-to-date bundle file used for server rendering, which is defined
# by config file value for config.server_bundle_js_file
ReactOnRails::ServerRenderingPool.reset_pool_if_server_bundle_was_modified
ReactOnRails::ServerRenderingPool.reset_pool_if_server_bundle_was_modified(react_component_name)

# Since this code is not inserted on a web page, we don't need to escape props
#
Expand Down
23 changes: 12 additions & 11 deletions lib/react_on_rails/server_rendering_js_code.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,21 @@ def server_rendering_component_js_code(
react_component_name: nil,
render_options: nil
)
ReactOnRails::Utils.with_trace(react_component_name) do
config_server_bundle_js = ReactOnRails.configuration.server_bundle_js_file

config_server_bundle_js = ReactOnRails.configuration.server_bundle_js_file
if render_options.prerender == true && config_server_bundle_js.blank?
msg = <<~MSG
The `prerender` option to allow Server Side Rendering is marked as true but the ReactOnRails configuration
for `server_bundle_js_file` is nil or not present in `config/initializers/react_on_rails.rb`.
Set `config.server_bundle_js_file` to your javascript bundle to allow server side rendering.
Read more at https://www.shakacode.com/react-on-rails/docs/guides/react-server-rendering/
MSG
raise ReactOnRails::Error, msg
end

if render_options.prerender == true && config_server_bundle_js.blank?
msg = <<~MSG
The `prerender` option to allow Server Side Rendering is marked as true but the ReactOnRails configuration
for `server_bundle_js_file` is nil or not present in `config/initializers/react_on_rails.rb`.
Set `config.server_bundle_js_file` to your javascript bundle to allow server side rendering.
Read more at https://www.shakacode.com/react-on-rails/docs/guides/react-server-rendering/
MSG
raise ReactOnRails::Error, msg
js_code_renderer.render(props_string, rails_context, redux_stores, react_component_name, render_options)
Comment on lines +21 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor tracing and error handling in server-side rendering.

The addition of tracing within the server_rendering_component_js_code method enhances debugging capabilities. However, the error message could be made more user-friendly and actionable.

-              Read more at https://www.shakacode.com/react-on-rails/docs/guides/react-server-rendering/
+              Please check the configuration or visit https://www.shakacode.com/react-on-rails/docs/guides/react-server-rendering/ for more information.
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ReactOnRails::Utils.with_trace(react_component_name) do
config_server_bundle_js = ReactOnRails.configuration.server_bundle_js_file
config_server_bundle_js = ReactOnRails.configuration.server_bundle_js_file
if render_options.prerender == true && config_server_bundle_js.blank?
msg = <<~MSG
The `prerender` option to allow Server Side Rendering is marked as true but the ReactOnRails configuration
for `server_bundle_js_file` is nil or not present in `config/initializers/react_on_rails.rb`.
Set `config.server_bundle_js_file` to your javascript bundle to allow server side rendering.
Read more at https://www.shakacode.com/react-on-rails/docs/guides/react-server-rendering/
MSG
raise ReactOnRails::Error, msg
end
if render_options.prerender == true && config_server_bundle_js.blank?
msg = <<~MSG
The `prerender` option to allow Server Side Rendering is marked as true but the ReactOnRails configuration
for `server_bundle_js_file` is nil or not present in `config/initializers/react_on_rails.rb`.
Set `config.server_bundle_js_file` to your javascript bundle to allow server side rendering.
Read more at https://www.shakacode.com/react-on-rails/docs/guides/react-server-rendering/
MSG
raise ReactOnRails::Error, msg
js_code_renderer.render(props_string, rails_context, redux_stores, react_component_name, render_options)
ReactOnRails::Utils.with_trace(react_component_name) do
config_server_bundle_js = ReactOnRails.configuration.server_bundle_js_file
if render_options.prerender == true && config_server_bundle_js.blank?
msg = <<~MSG
The `prerender` option to allow Server Side Rendering is marked as true but the ReactOnRails configuration
for `server_bundle_js_file` is nil or not present in `config/initializers/react_on_rails.rb`.
Set `config.server_bundle_js_file` to your javascript bundle to allow server side rendering.
Please check the configuration or visit https://www.shakacode.com/react-on-rails/docs/guides/react-server-rendering/ for more information.
MSG
raise ReactOnRails::Error, msg
end
js_code_renderer.render(props_string, rails_context, redux_stores, react_component_name, render_options)

end

js_code_renderer.render(props_string, rails_context, redux_stores, react_component_name, render_options)
end

def render(props_string, rails_context, redux_stores, react_component_name, render_options)
Expand Down
Loading
Loading