Skip to content

Commit

Permalink
Implement tracing
Browse files Browse the repository at this point in the history
  • Loading branch information
Mifrill authored and route committed May 9, 2022
1 parent 70b5d85 commit 24b9143
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 28 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ based on Ferrum and Mechanize.
* [Dialogs](https://github.com/rubycdp/ferrum#dialogs)
* [Animation](https://github.com/rubycdp/ferrum#animation)
* [Node](https://github.com/rubycdp/ferrum#node)
* [Tracing](https://github.com/rubycdp/ferrum#tracing)
* [Thread safety](https://github.com/rubycdp/ferrum#thread-safety)
* [Development](https://github.com/rubycdp/ferrum#development)
* [Contributing](https://github.com/rubycdp/ferrum#contributing)
Expand Down Expand Up @@ -1148,6 +1149,50 @@ browser.at_xpath("//*[select]").select(["1", "2"])
```


## Tracing

You can use `tracing.record` to create a trace file which can be opened in Chrome DevTools or [timeline viewer](https://chromedevtools.github.io/timeline-viewer/).

```ruby
browser.page.tracing.record(path: "trace.json") do
browser.go_to("https://www.google.com")
end
```

#### tracing.record(\*\*options) : `Hash`

- By default: returns Trace data from `Tracing.tracingComplete` event.
- When `path` specified: return `true` and store Trace data from `Tracing.tracingComplete` event to file.

* options `Hash`
* `:path` (String) - `String` to save a Trace data output on the disk, not specified by default.
* `:encoding` (Symbol) - `:base64` | `:binary` setting only for memory Trace data output, `:binary` by default.
* `:timeout` (Integer) - [Timeout of promise](https://github.com/ruby-concurrency/concurrent-ruby/blob/52c08fca13cc3811673ea2f6fdb244a0e42e0ebe/lib/concurrent-ruby/concurrent/promises.rb#L986) to wait til event `Tracing.Complete` triggered that fills buffer/file, `nil` by default, means "wait forever".
* `:screenshots` (Boolean) - When true - Captures screenshots in the trace, `false` by default.
* `:included_categories` (Array[String]) - An array of categories that be included to tracing data, by default:
```ruby
["devtools.timeline",
"v8.execute",
"disabled-by-default-devtools.timeline",
"disabled-by-default-devtools.timeline.frame",
"toplevel",
"blink.console",
"blink.user_timing",
"latencyInfo",
"disabled-by-default-devtools.timeline.stack",
"disabled-by-default-v8.cpu_profiler",
"disabled-by-default-v8.cpu_profiler.hires"]
```
* `:excluded_categories` (Array[String]) - An array of categories that be excluded from tracing data, by default:
```ruby
["*"]
```

See all categories by `browser.client.command("Tracing.getCategories")`

Only one trace can be active at a time per browser.


## Thread safety ##

Ferrum is fully thread-safe. You can create one browser or a few as you wish and
Expand Down
1 change: 1 addition & 0 deletions lib/ferrum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "ferrum/utils/platform"
require "ferrum/utils/elapsed_time"
require "ferrum/utils/attempt"
require "ferrum/utils/stream"
require "ferrum/errors"
require "ferrum/browser"
require "ferrum/node"
Expand Down
5 changes: 5 additions & 0 deletions lib/ferrum/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require "ferrum/page/frames"
require "ferrum/page/screenshot"
require "ferrum/page/animation"
require "ferrum/page/tracing"
require "ferrum/browser/client"

module Ferrum
Expand Down Expand Up @@ -215,6 +216,10 @@ def subscribed?(event)
@client.subscribed?(event)
end

def tracing
@tracing ||= Tracing.new(client: @client)
end

private

def subscribe
Expand Down
30 changes: 2 additions & 28 deletions lib/ferrum/page/screenshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,8 @@ def pdf(**opts)
path, encoding = common_options(**opts)
options = pdf_options(**opts).merge(transferMode: "ReturnAsStream")
handle = command("Page.printToPDF", **options).fetch("stream")

if path
stream_to_file(handle, path: path)
else
stream_to_memory(handle, encoding: encoding)
Utils::Stream.fetch(encoding: encoding, path: path) do |read_stream|
read_stream.call(client: self, handle: handle)
end
end

Expand Down Expand Up @@ -78,29 +75,6 @@ def save_file(path, data)
File.binwrite(path.to_s, data)
end

def stream_to_file(handle, path:)
File.open(path, "wb") { |f| stream_to(handle, f) }
true
end

def stream_to_memory(handle, encoding:)
data = String.new("") # Mutable string has << and compatible to File
stream_to(handle, data)
encoding == :base64 ? Base64.encode64(data) : data
end

def stream_to(handle, output)
loop do
result = command("IO.read", handle: handle, size: STREAM_CHUNK)

data_chunk = result["data"]
data_chunk = Base64.decode64(data_chunk) if result["base64Encoded"]
output << data_chunk

break if result["eof"]
end
end

def common_options(encoding: :base64, path: nil, **_)
encoding = encoding.to_sym
encoding = :binary if path
Expand Down
82 changes: 82 additions & 0 deletions lib/ferrum/page/tracing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

module Ferrum
class Page
class Tracing
INCLUDED_CATEGORIES = %w[
devtools.timeline
v8.execute
disabled-by-default-devtools.timeline
disabled-by-default-devtools.timeline.frame
toplevel
blink.console
blink.user_timing
latencyInfo
disabled-by-default-devtools.timeline.stack
disabled-by-default-v8.cpu_profiler
disabled-by-default-v8.cpu_profiler.hires
].freeze
EXCLUDED_CATEGORIES = %w[
*
].freeze

def initialize(client:)
@client = client
end

def record(options = {}, &block)
@options = {
timeout: nil,
screenshots: false,
encoding: :binary,
included_categories: INCLUDED_CATEGORIES,
excluded_categories: EXCLUDED_CATEGORIES,
**options
}
@promise = Concurrent::Promises.resolvable_future
subscribe_on_tracing_event
start
block.call
@client.command("Tracing.end")
@promise.value!(@options[:timeout])
end

private

def start
@client.command(
"Tracing.start",
transferMode: "ReturnAsStream",
traceConfig: {
includedCategories: included_categories,
excludedCategories: @options[:excluded_categories]
}
)
end

def included_categories
included_categories = @options[:included_categories]
if @options[:screenshots] == true
included_categories = @options[:included_categories] | ["disabled-by-default-devtools.screenshot"]
end
included_categories
end

def subscribe_on_tracing_event
@client.on("Tracing.tracingComplete") do |event, index|
next if index.to_i != 0

@promise.fulfill(stream(event.fetch("stream")))
rescue StandardError => e
@promise.reject(e)
end
end

def stream(handle)
Utils::Stream.fetch(encoding: @options[:encoding], path: @options[:path]) do |read_stream|
read_stream.call(client: @client, handle: handle)
end
end
end
end
end
43 changes: 43 additions & 0 deletions lib/ferrum/utils/stream.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module Ferrum
module Utils
module Stream
STREAM_CHUNK = 128 * 1024

module_function

def fetch(path:, encoding:, &block)
if path.nil?
stream_to_memory(encoding: encoding, &block)
else
stream_to_file(path: path, &block)
end
end

def stream_to_file(path:, &block)
File.open(path, "wb") { |f| stream_to(f, &block) }
true
end

def stream_to_memory(encoding:, &block)
data = String.new("") # Mutable string has << and compatible to File
stream_to(data, &block)
encoding == :base64 ? Base64.encode64(data) : data
end

def stream_to(output, &block)
loop do
read_stream = lambda do |client:, handle:|
client.command("IO.read", handle: handle, size: STREAM_CHUNK)
end
result = block.call(read_stream)
data_chunk = result["data"]
data_chunk = Base64.decode64(data_chunk) if result["base64Encoded"]
output << data_chunk
break if result["eof"]
end
end
end
end
end
Loading

0 comments on commit 24b9143

Please sign in to comment.