Skip to content

Commit

Permalink
Refactored v3 (giongto35#350)
Browse files Browse the repository at this point in the history
This PR contains refactored code.

**Changelog**
- Added new net code (the communication architecture was left intact).
- All network client IDs now have custom type `network.Uid` backed by github.com/rs/xid lib.
  ```
  The string representation of a UUID takes 32 bytes, and the new type will take just 16.
  Because of Golang JSON serialization problems with omitting zero-length empty slices (it can't) 
  and the need to use UID values as map keys (maps don't support slices as keys), 
  IDs are stored as strings (for now).
  ```
- A whole new WebSocket client/server implementation was added, as well as a new communication layer with synchronous and async call handlers.
  - WebSocket connections now support dedicated Ping/Pong frames as opposed to original ping text messages.
  - Used Gorilla WebSocket library doesn't allow concurrent (simultaneous) reads and writes, so this part was handled via send channel synchronization.
- New API structures can be found in the `pkg/api` folder.
- New communication protocol can be found in the `pkg/com/*` folder.
- Updated communication protocol is based on JSON-encoded messaging through WebSocket and has the following structure:
  ```
  Packet
    [id] string — a globally unique identification tag for the packet to track it trough a chain of requests.
    t uint8 — contains packet type information (i.e. INIT_PACKET, SDP_OFFER_PACKET, ...).
    [p] any — contains packet data (any type).

  Each packet is a text message in JSON-serialized form (WebSocket control frames obviously not).
  ```
  ```
  The main principle of this protocol and the duplex data exchange is:
  the one who initializes connection is called a client, and 
  the one who is being connected to is called a server. 
  With the current architecture, the coordinator is the server, the user browsers and workers are the clients.

            ____           ____
           ↓    ↑         ↑    ↓
     browser ⟶ coordinator ⟵ worker
       (c)          (s)         (c)

  One of the most crucial performance vise parts of these interactions is that 
  all the server-initiated calls to clients should be asynchronous!
  ```
  - In order to track synchronous calls (packets) with an asynchronous protocol, such as WebSocket, each packet may have an `id` that should be copied in all subsequent requests/responses.
  - The old `sessionID` param was replaced by `id` that should be stored inside the `p` (payload) part of the packet.
- It is possible to skip the default ping check for all connected workers on every user connection and just pick the first available with the new roundRobin param in the coordinator config file `coordinator.roundRobin: true/false`.
- Added a dedicated package for the system API (pkg/api/*).
- Added structured logging system (zerolog) for better logging and cloud services integration.
- Added a visual representation of the network message exchange in logs:
  ```
  ...
  01:00:01.1078 3f98 INF w → c Handshake ws://localhost:8000/wso
  01:00:01.1138  994 INF c ← w Handshake localhost:8000
  01:00:01.1148  994 INF c ← w Connect cid=cep.hrg
  01:00:01.1158  994 DBG c     connection id has been changed to cepl7obdrc3jv66kp2ug cid=cep.hrg
  01:00:01.1158 3f98 INF w → c Connect cid=cep.2ug
  01:00:01.1158  994 INF c     New worker / addr: localhost, ...
  01:00:01.1158 3f98 INF w     Connected to the coordinator localhost:8000 cid=cep.2ug
  01:00:02.5834  994 INF c ← u Handshake localhost:8000
  01:00:02.6175  994 INF c ← u Connect cid=cep.hs0
  01:00:02.6209  994 INF c     Search available workers cid=cep.hs0
  01:00:02.6214  994 INF c     Found next free worker cid=cep.hs0
  01:00:02.6220  994 INF c → u InitSession cid=cep.hs0
  01:00:02.6527  994 INF c ← u WebrtcInit cid=cep.hs0
  01:00:02.6527  994 INF c → w ᵇWebrtcInit cid=cep.hrg
  01:00:02.6537 3f98 INF w ← c WebrtcInit cid=cep.2ug
  01:00:02.6537 3f98 INF w     WebRTC start cid=cep.2ug
  ...
  ```
- Replaced a monstrous Prometheus metrics lib.
- Removed spflag dependency.
- Added new `version` config file param/constant for compatibility reasons.
- Bump the minimum required version for Go to 1.18 due to use of generics.
- Opus encoder now is cached and the default config is 96Kbps, complexity 5 (was 196Kbps, 8).
- Changed the default x264 quality parameters to `crf 23 / superfast / baseline` instead of `crf 17 / veryfast / main`.
- Added a separate WebRTC logging config param `webrtc.logLevel`.
- Worker now allocates much less memory.
- Optimized and fixed RGB to YUV converter.
- `--v=5` logging cmd flag was removed and replaced with the `debug` config parameter.


**Breaking changes (migration to v3)**
- Coordinator server API changes, see web/js/api/api.js.
- Coordinator client event API changes:
  - c `GAME_PLAYER_IDX_CHANGE` (string) -> `GAME_PLAYER_IDX` (number)
  - c `GAME_PLAYER_IDX` -> `GAME_PLAYER_IDX_SET`
  - c `MEDIA_STREAM_INITIALIZED` -> `WEBRTC_NEW_CONNECTION`
  - c `MEDIA_STREAM_SDP_AVAILABLE` -> `WEBRTC_SDP_OFFER`
  - c `MEDIA_STREAM_CANDIDATE_ADD` -> `WEBRTC_ICE_CANDIDATE_RECEIVED`
  - c `MEDIA_STREAM_CANDIDATE_FLUSH` -> `WEBRTC_ICE_CANDIDATES_FLUSH`
  - x `MEDIA_STREAM_READY` -> **removed**
  - c `CONNECTION_READY` -> `WEBRTC_CONNECTION_READY`
  - c `CONNECTION_CLOSED` -> `WEBRTC_CONNECTION_CLOSED`
  - c `GET_SERVER_LIST` -> `WORKER_LIST_FETCHED`
  - x `KEY_STATE_UPDATED` -> **removed**
  - n `WEBRTC_ICE_CANDIDATE_FOUND`
  - n `WEBRTC_SDP_ANSWER`
  - n `MESSAGE`
- `rtcp` module renamed to `webrtc`.
- Controller state equals Libretro controller state (changed order of bits), see: web/js/input/input.js.
- Added new `coordintaor.selector` config param that changes the selection algorithm for workers. By default it will select any free worker. Set this param to `ping` for the old behavior.
- Changed the name of the `webrtc.iceServers.url` config param to `webrtc.iceServers.urls`.
  • Loading branch information
sergystepanov authored Jan 9, 2023
1 parent 14b5894 commit 3bd959b
Show file tree
Hide file tree
Showing 213 changed files with 7,974 additions and 9,277 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/cd/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ services:

coordinator:
<<: *default-params
command: coordinator --v=5
command: coordinator
volumes:
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
Expand All @@ -28,7 +28,7 @@ services:
environment:
- MESA_GL_VERSION_OVERRIDE=3.3
entrypoint: [ "/bin/sh", "-c", "xvfb-run -a $$@", "" ]
command: worker --v=5 --zone=${ZONE:-}
command: worker --zone=${ZONE:-}
volumes:
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
- ${APP_DIR:-/cloud-game}/cores:/usr/local/share/cloud-game/assets/cores
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:

- uses: actions/setup-go@v2
with:
go-version: ^1.18
go-version: ^1.19

- name: Get Linux dev libraries and tools
if: matrix.os == 'ubuntu-latest'
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ _output/
./build
release/
vendor/
tests/
!tests/e2e/
*.exe

.dockerignore

Expand All @@ -75,3 +78,4 @@ fbneo/
hi/
nvram/
*.mcd

2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ RUN apt-get -qq update && apt-get -qq install --no-install-recommends -y \
&& rm -rf /var/lib/apt/lists/*

# go setup layer
ARG GO=go1.18.linux-amd64.tar.gz
ARG GO=go1.19.linux-amd64.tar.gz
RUN wget -q https://golang.org/dl/$GO \
&& rm -rf /usr/local/go \
&& tar -C /usr/local -xzf $GO \
Expand Down
72 changes: 19 additions & 53 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,59 +1,17 @@
# Makefile includes some useful commands to build or format incentives
# More commands could be added

# Variables
PROJECT = cloud-game
REPO_ROOT = github.com/giongto35
ROOT = ${REPO_ROOT}/${PROJECT}

CGO_CFLAGS='-g -O3 -funroll-loops'
CGO_LDFLAGS='-g -O3'

fmt:
@goimports -w cmd pkg tests
@gofmt -s -w cmd pkg tests

compile: fmt
@go install ./cmd/...

check: fmt
@golangci-lint run cmd/... pkg/...
# @staticcheck -checks="all,-S1*" ./cmd/... ./pkg/... ./tests/...

dep:
go mod download
# go mod tidy

# NOTE: there is problem with go mod vendor when it delete github.com/gen2brain/x264-go/x264c causing unable to build. https://github.com/golang/go/issues/26366
#build.cross: build
# CGO_ENABLED=1 GOOS=darwin GOARC=amd64 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/coordinator-darwin ./cmd/coordinator
# CGO_ENABLED=1 GOOS=darwin GOARC=amd64 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/worker-darwin ./cmd/worker
# CC=arm-linux-musleabihf-gcc GOOS=linux GOARC=amd64 CGO_ENABLED=1 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/coordinator-linu ./cmd/coordinator
# CC=arm-linux-musleabihf-gcc GOOS=linux GOARC=amd64 CGO_ENABLED=1 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/worker-linux ./cmd/worker

# A user can invoke tests in different ways:
# - make test runs all tests;
# - make test TEST_TIMEOUT=10 runs all tests with a timeout of 10 seconds;
# - make test TEST_PKG=./model/... only runs tests for the model package;
# - make test TEST_ARGS="-v -short" runs tests with the specified arguments;
# - make test-race runs tests with race detector enabled.
TEST_TIMEOUT = 60
TEST_PKGS ?= ./cmd/... ./pkg/...
TEST_TARGETS := test-short test-verbose test-race test-cover
.PHONY: $(TEST_TARGETS) test tests
test-short: TEST_ARGS=-short
test-verbose: TEST_ARGS=-v
test-race: TEST_ARGS=-race
test-cover: TEST_ARGS=-cover
$(TEST_TARGETS): test

test: compile
@go test -timeout $(TEST_TIMEOUT)s $(TEST_ARGS) $(TEST_PKGS)

test-e2e: compile
@go test ./tests/e2e/...

cover:
@go test -v -covermode=count -coverprofile=coverage.out $(TEST_PKGS)
# @$(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $(COVERALLS_TOKEN)

clean:
@rm -rf bin
@rm -rf build
Expand All @@ -62,25 +20,33 @@ clean:
build:
mkdir -p bin/
go build -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" -o bin/ ./cmd/coordinator
go build -buildmode=exe -tags static -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" $(EXT_WFLAGS) -o bin/ ./cmd/worker
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} \
go build -buildmode=exe -tags static \
-ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" $(EXT_WFLAGS) \
-o bin/ ./cmd/worker

verify-cores:
go test -run TestAllEmulatorRooms ./pkg/worker/room -v -renderFrames $(GL_CTX) -outputPath "../../../_rendered"
go test -run TestAllEmulatorRooms ./pkg/worker -v -renderFrames $(GL_CTX) -outputPath "../../_rendered"

dev.build: compile build

dev.build-local:
mkdir -p bin/
go build -o bin/ ./cmd/coordinator
go build -o bin/ ./cmd/worker
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} go build -o bin/ ./cmd/worker

dev.run: dev.build-local
./bin/coordinator --v=5 &
./bin/worker --v=5
./bin/coordinator & ./bin/worker

dev.run.debug:
go build -race -o bin/ ./cmd/coordinator
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} \
go build -race -gcflags=all=-d=checkptr -o bin/ ./cmd/worker
./bin/coordinator & ./bin/worker

dev.run-docker:
docker rm cloud-game-local -f || true
CLOUD_GAME_GAMES_PATH=$(PWD)/assets/games docker-compose up --build
docker-compose up --build

# RELEASE
# Builds the app for new release.
Expand All @@ -97,8 +63,8 @@ dev.run-docker:
# Config params:
# - RELEASE_DIR: the name of the output folder (default: release).
# - CONFIG_DIR: search dir for core config files.
# - DLIB_TOOL: the name of a dynamic lib copy tool (with params) (e.g., ldd -x -y; defalut: ldd).
# - DLIB_SEARCH_PATTERN: a grep filter of the output of the DLIB_TOOL (e.g., mylib.so; default: .*so).
# - DLIB_TOOL: the name of a dynamic lib copy tool (with params) (e.g., ldd -x -y; default: ldd).
# - DLIB_SEARCH_PATTERN: a grep filter of the output of the DLIB_TOOL (e.g., my_lib.so; default: .*so).
# Be aware that this search pattern will return only matched regular expression part and not the whole line.
# de. -> abc def ghj -> def
# Makefile special symbols should be escaped with \.
Expand Down
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ Because I only hosted the platform on limited servers in US East, US West, Eu, S
latency issues + connection problem. You can try hosting the service following the instruction the next section to have
a better sense of performance.

| Screenshot | Screenshot |
| :--------------------------------------------: | :--------------------------------------------: |
| ![screenshot](docs/img/landing-page-ps-hm.png) | ![screenshot](docs/img/landing-page-ps-x4.png) |
| ![screenshot](docs/img/landing-page-gb.png) | ![screenshot](docs/img/landing-page-front.png) |

## Feature

1. **Cloud gaming**: Game logic and storage is hosted on cloud service. It reduces the cumbersome of game
Expand Down
1 change: 1 addition & 0 deletions assets/cores/nestopia_libretro.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nestopia_audio_type=stereo
33 changes: 13 additions & 20 deletions cmd/coordinator/main.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,33 @@
package main

import (
"context"
goflag "flag"
"math/rand"
"time"

config "github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
"github.com/giongto35/cloud-game/v2/pkg/coordinator"
"github.com/giongto35/cloud-game/v2/pkg/logger"
"github.com/giongto35/cloud-game/v2/pkg/os"
"github.com/giongto35/cloud-game/v2/pkg/util/logging"
"github.com/golang/glog"
flag "github.com/spf13/pflag"
)

var Version = ""

func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
var Version = "?"

func main() {
rand.Seed(time.Now().UTC().UnixNano())
conf := config.NewConfig()
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
conf.ParseFlags()

logging.Init()
defer logging.Flush()
log := logger.NewConsole(conf.Coordinator.Debug, "c", true)

glog.Infof("[coordinator] version: %v", Version)
glog.V(4).Infof("[coordinator] Local configuration %+v", conf)
c := coordinator.New(conf)
log.Info().Msgf("version %s", Version)
log.Info().Msgf("conf version: %v", conf.Version)
if log.GetLevel() < logger.InfoLevel {
log.Debug().Msgf("config: %+v", conf)
}
c := coordinator.New(conf, log)
c.Start()

ctx, cancelCtx := context.WithCancel(context.Background())
defer c.Shutdown(ctx)
<-os.ExpectTermination()
cancelCtx()
if err := c.Stop(); err != nil {
log.Error().Err(err).Msg("service shutdown errors")
}
}
39 changes: 17 additions & 22 deletions cmd/worker/main.go
Original file line number Diff line number Diff line change
@@ -1,43 +1,38 @@
package main

import (
"context"
goflag "flag"
"math/rand"
"time"

config "github.com/giongto35/cloud-game/v2/pkg/config/worker"
"github.com/giongto35/cloud-game/v2/pkg/logger"
"github.com/giongto35/cloud-game/v2/pkg/os"
"github.com/giongto35/cloud-game/v2/pkg/thread"
"github.com/giongto35/cloud-game/v2/pkg/util/logging"
"github.com/giongto35/cloud-game/v2/pkg/worker"
"github.com/golang/glog"
flag "github.com/spf13/pflag"
"github.com/giongto35/cloud-game/v2/pkg/worker/thread"
)

var Version = ""

func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
var Version = "?"

func run() {
rand.Seed(time.Now().UTC().UnixNano())
conf := config.NewConfig()
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
conf.ParseFlags()

logging.Init()
defer logging.Flush()
log := logger.NewConsole(conf.Worker.Debug, "w", true)
log.Info().Msgf("version %s", Version)
log.Info().Msgf("conf version: %v", conf.Version)
if log.GetLevel() < logger.InfoLevel {
log.Debug().Msgf("config: %+v", conf)
}

glog.Infof("[worker] version: %v", Version)
glog.V(4).Infof("[worker] Local configuration %+v", conf)
wrk := worker.New(conf)
done := os.ExpectTermination()
wrk := worker.New(conf, log, done)
wrk.Start()

ctx, cancelCtx := context.WithCancel(context.Background())
defer wrk.Shutdown(ctx)
<-os.ExpectTermination()
cancelCtx()
<-done
time.Sleep(100 * time.Millisecond)
if err := wrk.Stop(); err != nil {
log.Error().Err(err).Msg("service shutdown errors")
}
}

func main() {
Expand Down
Loading

0 comments on commit 3bd959b

Please sign in to comment.