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

Add throttleNetwork on page #1094

Merged
merged 8 commits into from
Nov 13, 2023
Merged
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
1 change: 1 addition & 0 deletions browser/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ func mapPage(vu moduleVU, p *common.Page) mapping {
"setViewportSize": p.SetViewportSize,
"tap": p.Tap,
"textContent": p.TextContent,
"throttleNetwork": p.ThrottleNetwork,
"title": p.Title,
"touchscreen": rt.ToValue(p.GetTouchscreen()).ToObject(rt),
"type": p.Type,
Expand Down
1 change: 1 addition & 0 deletions browser/mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ type pageAPI interface {
SetViewportSize(viewportSize goja.Value)
Tap(selector string, opts goja.Value)
TextContent(selector string, opts goja.Value) string
ThrottleNetwork(common.NetworkProfile) error
Title() string
Type(selector string, text string, opts goja.Value)
Uncheck(selector string, opts goja.Value)
Expand Down
6 changes: 6 additions & 0 deletions common/frame_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,12 @@ func (fs *FrameSession) updateOffline(initial bool) {
}
}

func (fs *FrameSession) throttleNetwork(networkProfile NetworkProfile) error {
fs.logger.Debugf("NewFrameSession:throttleNetwork", "sid:%v tid:%v", fs.session.ID(), fs.targetID)

return fs.networkManager.ThrottleNetwork(networkProfile)
}

func (fs *FrameSession) updateRequestInterception(enable bool) error {
fs.logger.Debugf("NewFrameSession:updateRequestInterception",
"sid:%v tid:%v on:%v",
Expand Down
51 changes: 50 additions & 1 deletion common/network_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ import (
"github.com/dop251/goja"
)

// NetworkProfile is used in ThrottleNetwork.
type NetworkProfile struct {
// Minimum latency from request sent to response headers received (ms).
Latency float64

// Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.
Download float64

// Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.
Upload float64
}

// NewNetworkProfile creates a non-throttled network profile.
func NewNetworkProfile() NetworkProfile {
return NetworkProfile{
Latency: 0,
Download: -1,
Upload: -1,
}
}

// Credentials holds HTTP authentication credentials.
type Credentials struct {
Username string `js:"username"`
Expand Down Expand Up @@ -80,6 +101,7 @@ type NetworkManager struct {

extraHTTPHeaders map[string]string
offline bool
networkProfile NetworkProfile
userCacheDisabled bool
userReqInterceptionEnabled bool
protocolReqInterceptionEnabled bool
Expand Down Expand Up @@ -112,6 +134,7 @@ func NewNetworkManager(
reqIDToRequest: make(map[network.RequestID]*Request),
attemptedAuth: make(map[fetch.RequestID]bool),
extraHTTPHeaders: make(map[string]string),
networkProfile: NewNetworkProfile(),
}
m.initEvents()
if err := m.initDomains(); err != nil {
Expand Down Expand Up @@ -724,12 +747,38 @@ func (m *NetworkManager) SetOfflineMode(offline bool) {
}
m.offline = offline

action := network.EmulateNetworkConditions(m.offline, 0, -1, -1)
action := network.EmulateNetworkConditions(
m.offline,
m.networkProfile.Latency,
m.networkProfile.Download,
m.networkProfile.Upload,
)
if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil {
k6ext.Panic(m.ctx, "setting offline mode: %w", err)
}
}

// ThrottleNetwork changes the network attributes in chrome to simulate slower
// networks e.g. a slow 3G connection.
func (m *NetworkManager) ThrottleNetwork(networkProfile NetworkProfile) error {
if m.networkProfile == networkProfile {
return nil
}
m.networkProfile = networkProfile

action := network.EmulateNetworkConditions(
m.offline,
ka3de marked this conversation as resolved.
Show resolved Hide resolved
m.networkProfile.Latency,
m.networkProfile.Download,
m.networkProfile.Upload,
)
if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil {
return fmt.Errorf("throttling network: %w", err)
}

return nil
}

// SetUserAgent overrides the browser user agent string.
func (m *NetworkManager) SetUserAgent(userAgent string) {
action := emulation.SetUserAgentOverride(userAgent)
Expand Down
17 changes: 17 additions & 0 deletions common/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,23 @@ func (p *Page) Title() string {
return gojaValueToString(p.ctx, p.Evaluate(v))
}

// ThrottleNetwork will slow the network down to simulate a slow network e.g.
// simulating a slow 3G connection.
func (p *Page) ThrottleNetwork(networkProfile NetworkProfile) error {
p.logger.Debugf("Page:ThrottleNetwork", "sid:%v", p.sessionID())

p.frameSessionsMu.RLock()
defer p.frameSessionsMu.RUnlock()

for _, fs := range p.frameSessions {
if err := fs.throttleNetwork(networkProfile); err != nil {
return err
}
}

return nil
}

func (p *Page) Type(selector string, text string, opts goja.Value) {
p.logger.Debugf("Page:Type", "sid:%v selector:%s text:%s", p.sessionID(), selector, text)

Expand Down
56 changes: 56 additions & 0 deletions examples/throttle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { browser } from 'k6/x/browser';

export const options = {
scenarios: {
normal: {
executor: 'shared-iterations',
options: {
browser: {
type: 'chromium',
},
},
exec: 'normal',
},
throttled: {
executor: 'shared-iterations',
options: {
browser: {
type: 'chromium',
},
},
exec: 'throttled',
},
},
thresholds: {
'browser_http_req_duration{scenario:normal}': ['p(99)<500'],
'browser_http_req_duration{scenario:throttled}': ['p(99)<1500'],
},
}

export async function normal() {
const context = browser.newContext();
const page = context.newPage();

try {
await page.goto('https://test.k6.io/', { waitUntil: 'networkidle' });
} finally {
page.close();
}
}

export async function throttled() {
const context = browser.newContext();
const page = context.newPage();

try {
page.throttleNetwork({
latency: 750,
download: 250,
upload: 250,
});

await page.goto('https://test.k6.io/', { waitUntil: 'networkidle' });
} finally {
page.close();
}
}
98 changes: 98 additions & 0 deletions tests/page_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"encoding/json"
"fmt"
"image/png"
"io"
"net/http"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -1079,3 +1081,99 @@ func TestPageWaitForSelector(t *testing.T) {
})
}
}

func TestPageThrottleNetwork(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
networkProfile common.NetworkProfile
wantMinRoundTripDuration int64
}{
{
name: "none",
networkProfile: common.NetworkProfile{
Latency: 0,
Download: -1,
Upload: -1,
},
},
{
// In the ping.html file, an async ping request is made. The time it takes
// to perform the roundtrip of calling ping and getting the response is
// measured and used to assert that Latency has been correctly used.
name: "latency",
networkProfile: common.NetworkProfile{
Latency: 100,
Download: -1,
Upload: -1,
},
wantMinRoundTripDuration: 100,
},
{
// In the ping.html file, an async ping request is made, the ping response
// returns the request body (around a 1MB). The time it takes to perform the
// roundtrip of calling ping and getting the response body is measured and
// used to assert that Download has been correctly used.
name: "download",
networkProfile: common.NetworkProfile{
Latency: 0,
Download: 1000,
Upload: -1,
},
wantMinRoundTripDuration: 1000,
},
{
// In the ping.html file, an async ping request is made with around a 1MB body.
// The time it takes to perform the roundtrip of calling ping is measured
// and used to assert that Upload has been correctly used.
name: "upload",
networkProfile: common.NetworkProfile{
Latency: 0,
Download: -1,
Upload: 1000,
},
wantMinRoundTripDuration: 1000,
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

tb := newTestBrowser(t, withFileServer())

tb.withHandler("/ping", func(w http.ResponseWriter, req *http.Request) {
defer func() {
err := req.Body.Close()
require.NoError(t, err)
}()
bb, err := io.ReadAll(req.Body)
require.NoError(t, err)

fmt.Fprint(w, string(bb))
})

page := tb.NewPage(nil)

err := page.ThrottleNetwork(tc.networkProfile)
require.NoError(t, err)

_, err = page.Goto(tb.staticURL("ping.html"), nil)
require.NoError(t, err)

selector := `div[id="result"]`

// result selector only appears once the page gets a response
// from the async ping request.
_, err = page.WaitForSelector(selector, nil)
require.NoError(t, err)

resp := page.InnerText(selector, nil)
ms, err := strconv.ParseInt(resp, 10, 64)
require.NoError(t, err)
assert.GreaterOrEqual(t, ms, tc.wantMinRoundTripDuration)
})
}
}
29 changes: 29 additions & 0 deletions tests/static/ping.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<html>
<head>
<title>Ping duration test</title>
</head>
<body>
<div id="waiting">NA</div>
<script>
asd()

async function asd() {
var sendDate = (new Date()).getTime();

var receiveDate = 0;
await fetch('/ping', {
method: 'POST',
body: JSON.stringify('0'.repeat(1024))
})
.then(response => response.json())
.then(data => {
receiveDate = (new Date()).getTime();
})

var responseTimeMs = receiveDate - sendDate;

document.getElementById("waiting").innerHTML = `<div id="result">${responseTimeMs}</div>`;
}
</script>
</body>
</html>
Loading