GithubHelp home page GithubHelp logo

Comments (7)

mafredri avatar mafredri commented on May 13, 2024 1

This is how I'm currently using this in the benchmarks:

errc := make(chan error, b.N*3)
nw := rpcc.NoWait(errc)

for _, err := range []error{
	rpcc.Invoke(ctx, "Input.dispatchKeyEvent", down, nil, conn, nw),
	rpcc.Invoke(ctx, "Input.dispatchKeyEvent", char, nil, conn, nw),
	rpcc.Invoke(ctx, "Input.dispatchKeyEvent", up, nil, conn, nw),
} {
	if err != nil {
		b.Error(err)
	}
}

for i := 0; i < b.N*3; i++ {
	err := <-errc
	if err != nil {
		b.Error(err)
	}
}

Not sure how friendly the above is though, easy to forget to allocate enough buffer space in the channel. Also, you lose reference to which command failed. A nicer API (in that regard) might look something like:

calls := []*rpcc.Call{
	rpcc.NewCall("Input.dispatchKeyEvent", down, nil),
	rpcc.NewCall("Input.dispatchKeyEvent", char, nil),
	rpcc.NewCall("Input.dispatchKeyEvent", up, nil),
}
err := rpcc.Send(ctx, conn, calls...)
if err != nil {
	return err
}
for _, call := range calls {
	err := call.Wait()
	if err != nil {
		return err
	}
}

The downside here is increasing the API surface with more types (Call, NewCall and the function Send). Currently Call is unexported (rpcCall) which I find beneficial for internal usage. My thinking here is that having access to the call makes debugging / logging easier, e.g. finding out which call caused the error, etc. On the other hand, re-using a call is not possible, which might result in confusion and errors.

I'll keep thinking about this some more.

from cdp.

matthewmueller avatar matthewmueller commented on May 13, 2024

I should note that I haven't benchmarked this yet, but I'd imagine it should speed things up a bit. I can provide benchmarks if it'd help with the decision-making process :-)

from cdp.

mafredri avatar mafredri commented on May 13, 2024

Right now it would be possible to execute each command in a goroutine, but order isn't fully guaranteed, and this looks like a use-case where that guarantee is needed.

This kind of extension is something I've been keeping in mind, and we should be able to support it by extending e.g. Invoke with options:

func Invoke(ctx context.Context, method string, args, reply interface{}, conn *Conn, opts ...InvokeOpts) error { ... }

And to support invoking any command asynchronously, we'd change the interface slightly:

type Input interface {
    // ...
	DispatchKeyEvent(context.Context, *input.DispatchKeyEventArgs, opts ...rpcc.InvokeOpts) error

The reason I didn't introduce this yet is that I didn't have any options to put there :-P, but this will be fully backwards-compatible.

Usage (maybe?):

errc := make(chan error, 1)
_ = c.Input.DispatchKeyEvent(ctx, args, rpcc.InvokeAsync(errc))
// ...
err = <-errc

I'm undecided if the options should go in cdp or used directly from rpcc, perhaps cdp is better so the user has less reason to deal with rpcc.

PS. A benchmark would be much appreciated! :-)

from cdp.

mafredri avatar mafredri commented on May 13, 2024

Hey @matthewmueller, I've been giving this a lot of thought but I'm having a hard time finding a really good fit for this API change.

Do you have any ideas for how you would like to use it in practice?

I have some ideas that more or less could work, but I also want the behavior to be explicit and clear when reading the code.

// Option + Wait function.
nw := cdp.NoWait()
c.Page.Enable(ctx, nw)
c.Network.Enable(ctx, nil, nw)
c.DOM.Enable(ctx, nw)
shot, _ := c.Page.CaptureScreenshot(ctx, nil, nw) // Reply cannot be used yet.
err := cdp.Wait(ctx, nw)
// Safe to use `shot`.

// Async as a waitable + option.
async := cdp.Async()
c.Page.Enable(ctx, async.Option())
c.Network.Enable(ctx, nil, async.Option())
c.DOM.Enable(ctx, async.Option())
err := async.Wait(ctx) // or <-async.Done()

Or, running the async commands in their own scope, so to speak:

// NoWait provides non-blocking option, but blocks
// until all commands are completed.
var shot *page.CaptureScreenshotReply
err := cdp.NoWait(func(nw cdp.CommandOption) {
	c.Page.Enable(ctx, nw)
	c.Network.Enable(ctx, nil, nw)
	c.DOM.Enable(ctx, nw)
	shot, _ = c.Page.CaptureScreenshot(ctx, nil, nw)
})

// Separate client that does not wait for results.
err := cdp.NoWait(func(c *cdp.Client) {
	c.Page.Enable(ctx)
	c.Network.Enable(ctx, nil)
	c.DOM.Enable(ctx)
})

// Async as a part of context.
err := cdp.NoWait(ctx, func(ctx context.Context) {
	c.Page.Enable(ctx)
	c.Network.Enable(ctx, nil)
	c.DOM.Enable(ctx)
})

And finally, jokingly (or am I 😜😝):

// Full retard *JavaScript*.
async := cdp.AsyncContext(ctx)
p1 := c.Page.Enable(async) // Error, but p1.(*cdp.Promise) == OK ;P
p2 := c.Network.Enable(async, nil)
p3 := c.DOM.Enable(async)
err := cdp.Await(p1, p2, p3)

from cdp.

matthewmueller avatar matthewmueller commented on May 13, 2024

@mafredri Really appreciate the thought. I think you may be right that it may not warrant a large API change. The only use case I can really think of where this matters is keypresses, screencasting and scrolls and even then, only in certain cases (where you want to keep your event loop as speedy as possible).

It does make a difference though, I'm planning on doing something more official, but I was seeing around a 70ms improvement by just sending the keyboard event (to a local chrome process) and not waiting for the (empty) response.

I think one way forward is to just expose the raw RPCC API. The piece of the application I need this is in is quite performance sensitive, so I ended up writing a custom websocket implementation for that piece. I would have liked to just use the RPCC for this, but I didn't think it quite fit in the way I needed it. For everything else that I do, I won't need it to be performant like this and I'll definitely prefer just using the regular CDP API with the niceties of c.Input.DispatchKeyboardEvent() 😀

The interface that I ended up going with looks like this:

// Websocket interface
type Websocket interface {
        // Async (wait for write acknowledgement then return)
	Write(ctx context.Context, method string, params interface{}) error
        // RPC style (wait for ID to come back)
	Send(ctx context.Context, method string, params interface{}, reply interface{}) error
        // Subscriber has the same API as CDP's events
	Subscribe(ctx context.Context, method string) (*Subscriber, error)
        // Wait returns when the websocket dies (after a cleanup is tried)
	Wait() error
        // Try to close gracefully
	Close(error) error
}

I'm happy to share more of the implementation if you're interested. Like I mentioned, it's similar to CDP's RPCC, but a little too different for a PR haha.

from cdp.

mafredri avatar mafredri commented on May 13, 2024

The only use case I can really think of where this matters is keypresses, screencasting and scrolls and even then, only in certain cases (where you want to keep your event loop as speedy as possible).

I agree, I too see going async as a special use-case, but definitely one that should exist. In that regard, I think it's an OK first step to only support this via direct interaction with rpcc, at least in the beginning.

Currently the domain commands (strings) are not exported (e.g. "Network.enable") but I'll add those to make it slightly nicer to work with rpcc.

(Sidenote: I've also thought of turning these into types, e.g. network.Enable.Do(ctx, args). This would allow us to e.g. add a second method DoAsync, but this introduces two ways, client & command, to do the same thing which I'm not very excited about.)

It does make a difference though, I'm planning on doing something more official, but I was seeing around a 70ms improvement by just sending the keyboard event (to a local chrome process) and not waiting for the (empty) response.

Wow, that's not insignificant. This piqued my interest, I might have to write some benchmarks for rpcc myself 😆.

I would have liked to just use the RPCC for this, but I didn't think it quite fit in the way I needed it.

Could you expand on this a bit? I would love to see if we can match the offerings of rpcc to your requirements 😁. Is it more than the requirement to execute commands without waiting for the result?

I'm happy to share more of the implementation if you're interested. Like I mentioned, it's similar to CDP's RPCC, but a little too different for a PR haha.

Please do, I'm very curious if this can be aligned with rpcc.

PS. Speaking of performance considerations, I'm thinking of evaluating gobwas/ws as a replacement for gorilla/websocket.

from cdp.

mafredri avatar mafredri commented on May 13, 2024

There is indeed a huge difference, I wrote some benchmarks and ran some tests. Both the sync and async version are executing three Input.dispatchKeyEvent commands over rpcc (rawKeyDown, char and keyUp) to produce a newline.

Page: about:blank, no content:

$ go test -bench=Speed -run=Bench -benchtime 2s -v
BenchmarkRpccSpeed/Sync-4         	    2000	   1309191 ns/op	    1129 B/op	      33 allocs/op
BenchmarkRpccSpeed/Async-4        	   10000	    329931 ns/op	    1101 B/op	      27 allocs/op
PASS
ok  	github.com/mafredri/cdp	6.132s

Page about:blank, focused <textarea>:

The difference here is 102 (sync) vs 1102 (async) newlines inside the <textarea>.

$ go test -bench=Speed/Sync -run=Bench -benchtime 0.5s -v
BenchmarkRpccSpeed/Sync-4         	     100	   6668981 ns/op	    1152 B/op	      33 allocs/op
PASS
ok  	github.com/mafredri/cdp	0.699s

$ go test -bench=Speed/Async -run=Bench -benchtime 0.5s -v
BenchmarkRpccSpeed/Async-4         	    1000	   1787161 ns/op	    1001 B/op	      27 allocs/op
PASS
ok  	github.com/mafredri/cdp	1.900s

from cdp.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.