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

⚡️ [RUM-6813] Lazy load session replay #3152

Draft
wants to merge 5 commits into
base: v6
Choose a base branch
from

Conversation

amortemousque
Copy link
Contributor

@amortemousque amortemousque commented Nov 20, 2024

Motivation

Use dynamic imports to lazy load Session Replay, reducing the bundle size by 3KB (compressed) for users without Session Replay. Transitioning from Webpack, which adds overhead via __webpack_require__, to a new bundler could save additional bytes.

Changes

  • Configure ts module to ES2020
  • Update test app to target node and ES2018 (fix ssr)
  • Lazy load startRecording using dynamic import

Testing

  • Local
  • Staging
  • Unit
  • End to end

I have gone over the contributing documentation.

@amortemousque amortemousque changed the base branch from main to v6 November 20, 2024 16:19
@codecov-commenter
Copy link

codecov-commenter commented Nov 21, 2024

Codecov Report

Attention: Patch coverage is 94.11765% with 2 lines in your changes missing coverage. Please review.

Project coverage is 93.46%. Comparing base (839bae6) to head (2205ee9).
Report is 49 commits behind head on v6.

Files with missing lines Patch % Lines
packages/core/test/collectAsyncCalls.ts 88.23% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##               v6    #3152      +/-   ##
==========================================
- Coverage   93.49%   93.46%   -0.04%     
==========================================
  Files         275      275              
  Lines        7393     7419      +26     
  Branches     1675     1680       +5     
==========================================
+ Hits         6912     6934      +22     
- Misses        481      485       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.


🚨 Try these New Features:

@@ -2,7 +2,7 @@ const path = require('path')

module.exports = (_env, argv) => ({
entry: './app.ts',
target: ['web', 'es5'],
target: ['node', 'es2018'],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test app used to test if the SDK works for SSR was targeting the web. I fixed it to target node and es2018.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The test app used to test if the SDK works for SSR was targeting the web.

Is it what is tested? As I understand it is used to test the npm setup and it's injected in a html:

header += html` <script type="text/javascript" src="./app.js"></script> `

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I forgot that the test app is also used for npm e2e 😭

@@ -14,3 +14,17 @@ export function runOnReadyState(
const eventName = expectedReadyState === 'complete' ? DOM_EVENT.LOAD : DOM_EVENT.DOM_CONTENT_LOADED
return addEventListener(configuration, window, eventName, callback, { once: true })
}

export function asyncRunOnReadyState(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For now, I introduced an async version of runOnReadyState but if we agree on this lazy loading approach I'll refactor the original runOnReadyState to use promises.

Copy link

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 151.13 KiB 141.86 KiB -9495 B -6.14%
Logs 50.67 KiB 50.67 KiB 0 B 0.00%
Rum Slim 101.10 KiB 101.10 KiB 0 B 0.00%
Worker 25.16 KiB 25.16 KiB 0 B 0.00%
🚀 CPU Performance
Action Name Base Average Cpu Time (ms) Local Average Cpu Time (ms) 𝚫
addglobalcontext 0.002 0.002 0.000
addaction 0.034 0.038 0.004
addtiming 0.001 0.001 0.000
adderror 0.031 0.044 0.013
startstopsessionreplayrecording 0.764 0.008 -0.756
startview 0.920 0.341 -0.579
logmessage 0.022 0.018 -0.004
🧠 Memory Performance
Action Name Base Consumption Memory (bytes) Local Consumption Memory (bytes) 𝚫 (bytes)
addglobalcontext 8.38 KiB 9.73 KiB 1.35 KiB
addaction 41.34 KiB 41.98 KiB 651 B
addtiming 7.69 KiB 8.13 KiB 454 B
adderror 44.28 KiB 45.73 KiB 1.45 KiB
startstopsessionreplayrecording 5.85 KiB 6.09 KiB 249 B
startview 407.29 KiB 406.37 KiB -943 B
logmessage 42.05 KiB 42.32 KiB 278 B

🔗 RealWorld

@@ -0,0 +1,3 @@
export function lazyLoadRecorder() {
return import(/* webpackChunkName: "recorder" */ './startRecording').then((module) => module.startRecording)
Copy link
Collaborator

Choose a reason for hiding this comment

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

❓ question: ‏ I have a doubt, does this means that customer need to use webpack as well? or is this a stupid question?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not exactly, I used the webpackChunkName special comment to have a fixed chunk name to deploy our CDN files. For customers using NPM:

  • if they use webpack: the chunk will have a fix name
  • If they use another bundler this comment is ignored

}))
startRecordingSpy = jasmine.createSpy('startRecording')

// Workaround because using resolveTo(startRecordingSpy) is not was not working
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Workaround because using resolveTo(startRecordingSpy) is not was not working
// Workaround because using resolveTo(startRecordingSpy) was not working

Comment on lines +169 to +199
const doStart = async () => {
const [startRecordingImpl] = await Promise.all([
loadRecorder(),
asyncRunOnReadyState(configuration, 'interactive'),
])

if (state.status !== RecorderStatus.Starting) {
return
}

const deflateEncoder = getOrCreateDeflateEncoder()
if (!deflateEncoder) {
state = {
status: RecorderStatus.Stopped,
}
return
}

const { stop: stopRecording } = startRecordingImpl(
lifeCycle,
configuration,
sessionManager,
viewHistory,
deflateEncoder
)

state = {
status: RecorderStatus.Started,
stopRecording,
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

💬 suggestion: ‏ As we're using promise, I'll avoid having state all over the place. maybe something like this would be a bit more clean:

const doStart = async () => {
  const [startRecordingImpl] = await Promise.all([
    loadRecorder(),
    asyncRunOnReadyState(configuration, 'interactive'),
  ])

  if (state.status !== RecorderStatus.Starting) {
    return
  }

  const deflateEncoder = getOrCreateDeflateEncoder()
  if (!deflateEncoder) {
    throw new Error('deflate encoder not initialized!');
  }

  const { stop: stopRecording } = startRecordingImpl(
    lifeCycle,
    configuration,
    sessionManager,
    viewHistory,
    deflateEncoder
  )
  
  return stopRecording
}

And manage the state when you use it:

 doStart()
  .then(stop => {
    state = {
      status: RecorderStatus.Started,
      stopRecording: stop
    }
  })
  .catch(() => {
    state = {
      status: RecorderStatus.Stopped,
    }
  })

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting! I'll look at it

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.

3 participants