Skip to content

Separate internal and net.Conn implementations #20

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

Merged
merged 10 commits into from
Mar 13, 2017

Conversation

bign8
Copy link
Contributor

@bign8 bign8 commented Feb 16, 2017

Context: this is a follow up on the "no fmt" issues brought up recently. For those who aren't up to speed, feel free to check out #18 and #19.

Description: I dove into the import graphs, and as previously described, net and net/url also use the fmt package making it particularly difficult to remove the fmt dependency from the websocket package. On researching the code a little more, it appears the net packages are only used to fully implement the net.Conn interface. So this PR is separating the core JS wrapper logic from the net.Conn wrapper.

The newly created websocket.go file exposes the internal js wrapper to the top level package. This should allow for existing library consumers to not break when updating to use this version. With my personal experience of upgrading versions of packages, it's usually good to just bite the bullet and do this abstraction (making this file un-necessary), but I put this file in place to mitigate the concerns of any sticklers for API compatibility issues.

P.S: I am looking forward to collaborating with the gopherjs team!

Tasks

  • Propose a separation of internal and net.Conn code
  • Decide if this PR is a good idea
    • Decide if websocket.go is necessary (it's not)
      • Fix New to produce a WebSocket instead of a core.WebSocket
    • Add documentation so others can find gopherjs/websocket/core
  • Get Maintainers Approval
  • Profit!!!

Requested Reviewers

@flimzy
Copy link
Member

flimzy commented Feb 16, 2017

If net is only included to reference its interfaces as return values, a simple solution is to duplicate those interfaces in the websocket package. See my simple diff.

I'm not suggesting this is necessarily a good idea, however. It means we have to stay on top of keeping those two interfaces (net.Conn and net.Addr) in sync with the standard library, any time there is a change. It also means it would become impossible (or at minimum, non-trivial) to support multiple versions of Go with the websocket package.

So, take this observation with a grain of salt. It's not a suggestion; it's only an observation.

I don't use this package, so I don't have any vested interest in any outcome here.

@bign8
Copy link
Contributor Author

bign8 commented Feb 16, 2017

Good idea @flimzy! Only problem is that LocalAddr() and RemoteAddr() return a net.Addr interface, not a websocket.Addr. So I don't think the following assertion compiles correctly because those methods return different interfaces (even though they are the same). If I'm wrong, I think cloned interface method is a much better plan, it just requires home growing the url.Parse method. But I'm pretty sure this won't compile.

var _ net.Conn = (*conn)(nil)

Update: not a direct apply of your patch, but the basic idea applied

# github.com/gopherjs/websocket
./conn.go:19: cannot use (*conn)(nil) (type *conn) as type net.Conn in assignment:
*conn does not implement net.Conn (wrong type for LocalAddr method)
have LocalAddr() Addr
want LocalAddr() net.Addr
./conn.go:164: cannot use conn (type *conn) as type net.Conn in return argument:
*conn does not implement net.Conn (wrong type for LocalAddr method)
have LocalAddr() Addr
want LocalAddr() net.Addr

@dmitshur
Copy link
Member

dmitshur commented Feb 23, 2017

Thanks a lot for wanting to contribute, @bign8!

In general, I am very wary of these "no fmt" efforts. You can read golang/go#15583 for more background information. But I understand, given how big net/http has become recently, the desire to try to keep it out if it's unused.

In order to be able to solve this task analytically:

  • Decide if this PR is a good idea

Can you motivate it by posting the effects of doing this change. For example, post the generated file size of a program that imports websocket package before and after the change.

Once we have some concrete numbers of the savings, it's easier to think about how much effort we want to invest into re-arranging code.

@bign8
Copy link
Contributor Author

bign8 commented Feb 25, 2017

Great idea @shurcooL; Let the experimentation begin!

Context

$ go version
go version go1.7.3 darwin/amd64
$ gopherjs version
GopherJS 1.7-1

Notes

  • The following verbosity is to assert reproducibility.
  • The // +build ignore prefix for the files is because main.go was in the root of this repository.

Tests

Master (control)

$ git rev-parse HEAD && gopherjs build main.go && ls -g main.js && gopherjs build -m main.go && ls -g main.js && cat main.go
edfe1438a4184bea0b3f9e35fd77969061676d9c
-rw-r--r--  1 staff  882744 Feb 24 23:23 main.js
-rw-r--r--  1 staff  608852 Feb 24 23:23 main.js
// +build ignore

package main

import "github.com/gopherjs/websocket"

func main() { websocket.New("ws://example.org/ws") }

No-FMT With Wrapper

$ git rev-parse HEAD && gopherjs build main.go && ls -g main.js && gopherjs build -m main.go && ls -g main.js && cat main.go
f2d9e22bd2e3459bce83243b82931cc05c9aa75f
-rw-r--r--  1 staff  884335 Feb 24 23:25 main.js
-rw-r--r--  1 staff  609973 Feb 24 23:25 main.js
// +build ignore

package main

import "github.com/gopherjs/websocket"

func main() { websocket.New("ws://example.org/ws") }

No-FMT With Core

$ git rev-parse HEAD && gopherjs build main.go && ls -g main.js && gopherjs build -m main.go && ls -g main.js && cat main.go
f2d9e22bd2e3459bce83243b82931cc05c9aa75f
-rw-r--r--  1 staff  75575 Feb 24 23:26 main.js
-rw-r--r--  1 staff  56145 Feb 24 23:26 main.js
// +build ignore

package main

import websocket "github.com/gopherjs/websocket/core"

func main() { websocket.New("ws://example.org/ws") }

Summary

Master Wrapper Core
Normal Size 882,744 884,335 75,575
% Change* 882,744 0.18023345% -91.43862773%
Minified Size 608,852 609,973 56,145
% Change* 608,852 0.18411699% -90.77854716%

* Calculated with http://percent-change.com/

For those who are visually inclined (or because a real experiment requires plots 😉):
comparison

@mjohnson9
Copy link
Member

Convincing results if you're only using websocket in your program, but I'm skeptical after reading the reasoning in golang/go#15583.

In an actual program, how often is fmt or net not imported? It seems that you'd have to carefully avoid most stdlib packages.

@bign8
Copy link
Contributor Author

bign8 commented Feb 25, 2017

Excellent question @nightexcessive!

Of the 155 std packages, 83 of them uses fmt. source

Yes, larger programs have a higher likelihood of consuming the fmt package. But, for those of us not using the stdlib (or choosing to avoid it), the choice to implement the net.Conn interface does lead to the stdlib dependency chain.

One of my primary uses of gopherjs is to generate communication libraries between client and server modules. It does require some work and verification to assert the the compiled packages don't use the stdlib, but as seen above, the compiled size benefits are well worth a few minutes of my time.

The changes I'm currently proposing allow for those who don't care about the size of their generated source (or require the net.Conn implementation) to use the code as it stands; While those that choose to optimize can add /core to their github.com/gohperjs/websocket imports and have a significant size reduction (iff not otherwise entangling with stdlib packages).

I understand this topic easily degrades to a conversation between the utility and performance of code. Where, in this instance, utility is making websocket the Swiss army knife of code, supporting all possible uses and performance as the resulting compiled js size. Where #20 would potentially allow the consumer to choose.

@mjohnson9
Copy link
Member

Your argument is convincing. I will review the code shortly.

Copy link
Member

@mjohnson9 mjohnson9 left a comment

Choose a reason for hiding this comment

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

See code comments for requested changes

conn.go Outdated
@@ -13,6 +13,7 @@ import (
"time"

"github.com/gopherjs/gopherjs/js"
core "github.com/gopherjs/websocket/core"
Copy link
Member

Choose a reason for hiding this comment

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

Core prefix is not necessary

websocket.go Outdated

package websocket

import core "github.com/gopherjs/websocket/core"
Copy link
Member

Choose a reason for hiding this comment

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

Core prefix is not necessary

@mjohnson9
Copy link
Member

mjohnson9 commented Feb 25, 2017

Reviewed the code; only very minor changes requested.

mjohnson9
mjohnson9 previously approved these changes Feb 25, 2017
@mjohnson9
Copy link
Member

LGTM.

It seems that this offers flexibility for an important demographic without making the code significantly more difficult to maintain.

@shurcooL has voiced his concerns and, as the other primary contributor on this project, I'd want his approval before this is merged.

@dmitshur
Copy link
Member

dmitshur commented Feb 28, 2017

Thank you for providing those numbers (and graphs!) @bign8, that's very helpful.

I think based on that, and your rationale in #20 (comment), I'm in agreement that we can support a pure-JS wrapper package that doesn't pull in all of net/http.

Doing so is quite easy and I can imagine one that cares strongly about generated sizes maintaining their own modified fork of this library. But it seems to be easy enough for us to provide that here, in the upstream repo, so I don't mind doing that.

Let's figure out the best way to do it.

From your PR so far, it really seems that it's very viable to have a package that wraps the websocket API that doesn't even need fmt package.

If that weren't the case, I think the current setup of having main websocket package unmodified, providing both high- and low-level bindings, in addition to a separate package that's low-level only, would be the best/only way to go.

However, it seems to me so far that we can do better. The problem with the above setup is that IMO it becomes unfriendly towards people seeing the package for the first time, because they would think "I just want to use the low-level API, but it's provided in both packages... which of the two packages should I use?"

An idea I'd like us to consider is the following. Let's consider splitting the high- and low-level bindings into two separate packages, without overlap.

The idea is that users who want low-level only can use that small package, and those who want a high-level implementation that implements net.Conn can use the other package. There's no overlap, and it's very clean.

What I like about doing that is that it would IMO improve the high-level package by making its API smaller, it would be just the Dial method and nothing more.

We can update the documentation to have the high-level package describe how to use it, and move the low-level API docs into its own package. We can add a sentence that points out the low-level bindings, something like:

// There are low-level bindings that use the typical JavaScript idioms
// available in package websocketjs.

This brings me to another point, I'd like to find a better name for the low-level bindings than core. Core is very generic and doesn't mean anything. core.New doesn't sound like it creates a websocket. core.WebSocket is also not ideal.

It's kinda hard to think of a better replacement, but right now my idea is websocketjs. So the two packages could be:

  • github.com/gopherjs/websocket - Package websocket provides high-level bindings for the browser's WebSocket API.
  • github.com/gopherjs/websocket/websocketjs - Package websocketjs provides low-level bindings for the browser's WebSocket API.

I'm still not completely sure doing the split is better, because perhaps some people would find it friendlier to see both APIs in one package. But, hopefully that's not the case. What do you people think?

@bign8
Copy link
Contributor Author

bign8 commented Mar 2, 2017

@shurcooL Updated as suggested (sorry for the delay).

Copy link
Member

@dmitshur dmitshur left a comment

Choose a reason for hiding this comment

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

Sorry for the delay in reviewing this.

This mostly looks good, but there are a few inconsistencies in the documentation that need to be ironed out. See inline comments.

Additionally, we should update the README to be up to date. It's fine to keep both examples there, just update the text to refer to each of the two packages. The current README assumes both bindings live in the same top-level package.

doc.go Outdated

ws, err := websocket.New("ws://localhost/socket") // Does not block.
ws, err := websocketjs.New("ws://localhost/socket") // Does not block.
if err != nil { panic(err) }

onOpen := func(ev *js.Object) {
Copy link
Member

Choose a reason for hiding this comment

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

I think we should move this entire example for websocketjs into that package, rather than keeping it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 4293b3c

@@ -2,11 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.

package websocket
Copy link
Member

Choose a reason for hiding this comment

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

It can be something like this:

/*
Package websocketjs provides low-level bindings for the browser's WebSocket API.

These bindings allow for using typical JavaScript idioms,
such as adding event listeners with callbacks. For example:

	ws, err := websocketjs.New("ws://localhost/socket") // Does not block.
	if err != nil { ... }
 
	onOpen := func(ev *js.Object) {
		err := ws.Send([]byte("Hello!")) // Send a binary frame.
		err := ws.Send("Hello!")         // Send a text frame.
	}

	ws.AddEventListener("open", false, onOpen)
	ws.AddEventListener("message", false, onMessage)
	ws.AddEventListener("close", false, onClose)
	ws.AddEventListener("error", false, onError)
 
	err = ws.Close()
	if err != nil { ... }
*/
package websocketjs

I've also replaced if err != nil { panic(err) } with if err != nil { ... }. I think it's clear enough that it means you should handle errors, and it's better not to show panic(err) as a way of doing that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 4293b3c

doc.go Outdated
@@ -21,9 +21,9 @@ The high-level bindings act like a regular net.Conn. They can be used as such. F
err = c.Close()
Copy link
Member

Choose a reason for hiding this comment

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

At the very top, this package comment still says:

Package websocket provides high- and low-level bindings for the browser's WebSocket API.

It should be updated. How about:

Package websocket provides high-level bindings for the browser's WebSocket API.

These bindings offer a Dial function that returns a regular net.Conn.
They can be used similar to net package. For example:

	...

You can replace if err != nil { panic(err) } with if err != nil { ... } here as well, if you're okay with that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 4293b3c

- Updating README.md to reflect websocket and websocketjs package distinctions
- Moving low-level example to websocketjs
- Replace documentation panic error handling with "..."
@bign8
Copy link
Contributor Author

bign8 commented Mar 12, 2017

@shurcooL All good suggestions!

Here are what the godoc pages look like locally (godoc -http=:<your-favorite-port-here>)

@dmitshur
Copy link
Member

This looks good.

@bign8, I have a few minor documentation tweaks in mind. To apply them to skip round-trip review comments latency, I'll push them to this branch if you don't mind. Please look it over after and feel free to adjust or let me know what you think.

Use patterns similar to net/http package documentation for displaying
error handling example. See https://godoc.org/net/http examples on top.

Rename c to conn to make the high level example more readable.

Try to adjust language to be more readable and consistent.

Process buf[:n] data first, handle error second. This is correct
according to Read method documentation, see
https://godoc.org/io#Reader.Read:

	Callers should always process the n > 0 bytes returned before
	considering the error err. Doing so correctly handles I/O errors
	that happen after reading some bytes and also both of the allowed
	EOF behaviors.

Remove outdated reference to Conn from websocketjs package.
@dmitshur
Copy link
Member

dmitshur commented Mar 12, 2017

@bign8 I've pushed e990143, can you look over it and let me know how it looks to you. Read its commit message for rationale for changes.

I think this looks good to merge from my side now. Thanks a lot for your work on this.

README.md Outdated
@@ -3,33 +3,42 @@ websocket

Packages websocket and websocketjs provide high- and low-level bindings for the browser's WebSocket API (respectively).

The high-level bindings act like a regular `net.Conn`. They can be used as such. For example:
The high-level bindings offer a Dial function that returns a regular net.Conn.
It can be used similar to net package.
Copy link
Member

Choose a reason for hiding this comment

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

I know everybody hates a grammar nazi, but this should read "It can be used similarly to net package", as it describes an action, so an adverb is called for.

Copy link
Member

Choose a reason for hiding this comment

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

I know everybody hates a grammar nazi

I do not, thank you for the suggestion for improvement. I am happy to apply it.

doc.go Outdated
@@ -6,20 +6,24 @@
Package websocket provides high-level bindings for the browser's WebSocket API.

These bindings offer a Dial function that returns a regular net.Conn.
They can be used similar to net package. For example:
It can be used similar to net package.
Copy link
Member

Choose a reason for hiding this comment

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

Same comment here.

It describes an action, so an adverb is called for.
@bign8
Copy link
Contributor Author

bign8 commented Mar 12, 2017

👍 on 149a4c9 and e990143. Documentation looks awesome! thanks @shurcooL!

Copy link
Member

@dmitshur dmitshur left a comment

Choose a reason for hiding this comment

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

LGTM.

@dmitshur
Copy link
Member

@nightexcessive Do you want to take another look after all the changes?

If it looks good to you too, let's merge this.

@dmitshur
Copy link
Member

Oh, I just noticed that test/test/index.go hasn't been updated to use the new API. @bign8, can you work on that?

@bign8
Copy link
Contributor Author

bign8 commented Mar 12, 2017

Sure! Let me see if I can get that running

@bign8
Copy link
Contributor Author

bign8 commented Mar 12, 2017

@shurcooL and @nightexcessive Tests run fine on my machine now.

cd test
go get ./...
go generate ./...
go run ./server.go
open localhost:3000

screen shot 2017-03-12 at 2 01 15 pm

@mjohnson9 mjohnson9 self-requested a review March 12, 2017 20:15
@mjohnson9
Copy link
Member

Will review ASAP.

@mjohnson9 mjohnson9 dismissed their stale review March 12, 2017 23:03

Re-reviewing

Copy link
Member

@mjohnson9 mjohnson9 left a comment

Choose a reason for hiding this comment

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

LGTM. You're very thorough, @shurcooL, far moreso than myself. 👍

@dmitshur
Copy link
Member

Thanks @bign8 for contributing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants