-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Latency improved for activity keyer and tree composer #5465
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
base: main
Are you sure you want to change the base?
Changes from all commits
159993b
921afa9
e413d67
eb4ff2c
a9f8c6c
06077cd
02e5b53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -186,6 +186,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/ | |
- [`[email protected]`](https://npmjs.com/package/webpack-cli/) | ||
- [`[email protected]`](https://npmjs.com/package/webpack/) | ||
- Fixed [#5446](https://github.com/microsoft/BotFramework-WebChat/issues/5446). Embedded `uuid` so `microsoft-cognitiveservices-speech-sdk` do not need to use dynamic loading, as this could fail in Webpack 4 environment, in PR [#5445](https://github.com/microsoft/BotFramework-WebChat/pull/5445), by [@compulim](https://github.com/compulim) | ||
- Improved latency of `ActivityKeyerComposer` and `ActivityTreeComposer` in PR [#5465](https://github.com/microsoft/BotFramework-WebChat/pull/5465) by [@pranavjoshi001](https://github.com/pranavjoshi001) | ||
|
||
### Fixed | ||
|
||
|
@@ -216,6 +217,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/ | |
# Removed | ||
|
||
- Deprecating `disabled` props and `useDisabled` hook in favor of new `uiState` props and `useUIState` hook, in PR [#5276](https://github.com/microsoft/BotFramework-WebChat/pull/5276), by [@compulim](https://github.com/compulim) | ||
- Deleted `ActivitListenerComposer` and `useUpsertedActivities` hook in PR [#5465](https://github.com/microsoft/BotFramework-WebChat/pull/5465) by [@pranavjoshi001](https://github.com/pranavjoshi001) | ||
|
||
## [4.18.0] - 2024-07-10 | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import useReduceActivities from '../providers/ActivityTyping/private/useReduceActivities'; | ||
|
||
export default useReduceActivities; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,7 @@ type KeyToActivitiesMap = Map<string, readonly WebChatActivity[]>; | |
* | ||
* Local key are only persisted in memory. On refresh, they will be a new random key. | ||
*/ | ||
// eslint-disable-next-line complexity | ||
const ActivityKeyerComposer = ({ children }: Readonly<{ children?: ReactNode | undefined }>) => { | ||
const existingContext = useActivityKeyerContext(false); | ||
|
||
|
@@ -43,53 +44,36 @@ const ActivityKeyerComposer = ({ children }: Readonly<{ children?: ReactNode | u | |
const activityToKeyMapRef = useRef<Readonly<ActivityToKeyMap>>(Object.freeze(new Map())); | ||
const clientActivityIdToKeyMapRef = useRef<Readonly<ClientActivityIdToKeyMap>>(Object.freeze(new Map())); | ||
const keyToActivitiesMapRef = useRef<Readonly<KeyToActivitiesMap>>(Object.freeze(new Map())); | ||
const lastProcessedIndexRef = useRef(0); | ||
|
||
// TODO: [P1] `useMemoWithPrevious` to check and cache the resulting array if it hasn't changed. | ||
const activityKeysState = useMemo<readonly [readonly string[]]>(() => { | ||
const { current: activityIdToKeyMap } = activityIdToKeyMapRef; | ||
const { current: activityToKeyMap } = activityToKeyMapRef; | ||
const { current: clientActivityIdToKeyMap } = clientActivityIdToKeyMapRef; | ||
const nextActivityIdToKeyMap: ActivityIdToKeyMap = new Map(); | ||
const nextActivityKeys: Set<string> = new Set(); | ||
const nextActivityToKeyMap: ActivityToKeyMap = new Map(); | ||
const nextClientActivityIdToKeyMap: ClientActivityIdToKeyMap = new Map(); | ||
const nextKeyToActivitiesMap: KeyToActivitiesMap = new Map(); | ||
|
||
activities.forEach(activity => { | ||
// Process new activities (if any) | ||
if (activities.length > lastProcessedIndexRef.current) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The It could be updated and become Can we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried with |
||
for (let i = lastProcessedIndexRef.current; i < activities.length; i++) { | ||
// eslint-disable-next-line security/detect-object-injection | ||
const activity = activities[i]; | ||
const activityId = getActivityId(activity); | ||
if (!activityId) { | ||
break; | ||
} | ||
const clientActivityId = getClientActivityId(activity); | ||
const typingActivityId = getActivityLivestreamingMetadata(activity)?.sessionId; | ||
|
||
const key = | ||
(clientActivityId && | ||
(clientActivityIdToKeyMap.get(clientActivityId) || nextClientActivityIdToKeyMap.get(clientActivityId))) || | ||
(typingActivityId && | ||
(activityIdToKeyMap.get(typingActivityId) || nextActivityIdToKeyMap.get(typingActivityId))) || | ||
(activityId && (activityIdToKeyMap.get(activityId) || nextActivityIdToKeyMap.get(activityId))) || | ||
activityToKeyMap.get(activity) || | ||
nextActivityToKeyMap.get(activity) || | ||
(clientActivityId && clientActivityIdToKeyMapRef.current.get(clientActivityId)) || | ||
(typingActivityId && activityIdToKeyMapRef.current.get(typingActivityId)) || | ||
(activityId && activityIdToKeyMapRef.current.get(activityId)) || | ||
activityToKeyMapRef.current.get(activity) || | ||
uniqueId(); | ||
|
||
activityId && nextActivityIdToKeyMap.set(activityId, key); | ||
clientActivityId && nextClientActivityIdToKeyMap.set(clientActivityId, key); | ||
nextActivityToKeyMap.set(activity, key); | ||
nextActivityKeys.add(key); | ||
|
||
const activities = nextKeyToActivitiesMap.has(key) ? [...nextKeyToActivitiesMap.get(key)] : []; | ||
|
||
activities.push(activity); | ||
nextKeyToActivitiesMap.set(key, Object.freeze(activities)); | ||
}); | ||
|
||
activityIdToKeyMapRef.current = Object.freeze(nextActivityIdToKeyMap); | ||
activityToKeyMapRef.current = Object.freeze(nextActivityToKeyMap); | ||
clientActivityIdToKeyMapRef.current = Object.freeze(nextClientActivityIdToKeyMap); | ||
keyToActivitiesMapRef.current = Object.freeze(nextKeyToActivitiesMap); | ||
|
||
// `nextActivityKeys` could potentially same as `prevActivityKeys` despite reference differences, we should memoize it. | ||
return Object.freeze([Object.freeze([...nextActivityKeys.values()])]) as readonly [readonly string[]]; | ||
}, [activities, activityIdToKeyMapRef, activityToKeyMapRef, clientActivityIdToKeyMapRef, keyToActivitiesMapRef]); | ||
activityId && activityIdToKeyMapRef.current.set(activityId, key); | ||
clientActivityId && clientActivityIdToKeyMapRef.current.set(clientActivityId, key); | ||
|
||
activityToKeyMapRef.current.set(activity, key); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am curious why this would work. In the new code, |
||
const existingList = keyToActivitiesMapRef.current.get(key) ?? []; | ||
keyToActivitiesMapRef.current.set(key, Object.freeze([...existingList, activity])); | ||
lastProcessedIndexRef.current = activities.length; | ||
} | ||
} | ||
const getActivitiesByKey: (key?: string | undefined) => readonly WebChatActivity[] | undefined = useCallback( | ||
(key?: string | undefined): readonly WebChatActivity[] | undefined => key && keyToActivitiesMapRef.current.get(key), | ||
[keyToActivitiesMapRef] | ||
|
@@ -110,9 +94,18 @@ const ActivityKeyerComposer = ({ children }: Readonly<{ children?: ReactNode | u | |
[activityIdToKeyMapRef] | ||
); | ||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
const activityKeys = useMemo(() => Object.freeze([...keyToActivitiesMapRef.current.keys()]), [activities.length]); // we want to update the keys when activities change | ||
|
||
const contextValue = useMemo<ActivityKeyerContextType>( | ||
() => ({ activityKeysState, getActivityByKey, getActivitiesByKey, getKeyByActivity, getKeyByActivityId }), | ||
[activityKeysState, getActivitiesByKey, getActivityByKey, getKeyByActivity, getKeyByActivityId] | ||
() => ({ | ||
activityKeysState: [activityKeys], | ||
getActivityByKey, | ||
getActivitiesByKey, | ||
getKeyByActivity, | ||
getKeyByActivityId | ||
}), | ||
[activityKeys, getActivitiesByKey, getActivityByKey, getKeyByActivity, getKeyByActivityId] | ||
); | ||
|
||
const { length: numActivities } = activities; | ||
|
@@ -149,7 +142,7 @@ const ActivityKeyerComposer = ({ children }: Readonly<{ children?: ReactNode | u | |
); | ||
} | ||
|
||
if (activityKeysState[0].length !== keyToActivitiesMapRef.current.size) { | ||
if (activityKeys.length !== keyToActivitiesMapRef.current.size) { | ||
console.warn( | ||
'botframework-webchat internal assertion: "activityKeys.length" should be same as "keyToActivitiesMap.size".' | ||
); | ||
|
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import { hooks, type ActivityComponentFactory } from 'botframework-webchat-api'; | ||
import type { WebChatActivity } from 'botframework-webchat-core'; | ||
import React, { useMemo, type ReactNode } from 'react'; | ||
import React, { useMemo, useRef, type ReactNode } from 'react'; | ||
|
||
import useMemoWithPrevious from '../../hooks/internal/useMemoWithPrevious'; | ||
import ActivityTreeContext from './private/Context'; | ||
|
@@ -13,7 +13,14 @@ import type { ActivityTreeContextType } from './private/Context'; | |
|
||
type ActivityTreeComposerProps = Readonly<{ children?: ReactNode | undefined }>; | ||
|
||
const { useActivities, useActivityKeys, useCreateActivityRenderer, useGetActivitiesByKey, useGetKeyByActivity } = hooks; | ||
const { | ||
useActivities, | ||
useActivityKeys, | ||
useCreateActivityRenderer, | ||
useGetActivitiesByKey, | ||
useGetKeyByActivity, | ||
useReduceActivities | ||
} = hooks; | ||
|
||
const ActivityTreeComposer = ({ children }: ActivityTreeComposerProps) => { | ||
const existingContext = useActivityTreeContext(false); | ||
|
@@ -27,26 +34,35 @@ const ActivityTreeComposer = ({ children }: ActivityTreeComposerProps) => { | |
const getKeyByActivity = useGetKeyByActivity(); | ||
const activityKeys = useActivityKeys(); | ||
|
||
const activities = useMemo<readonly WebChatActivity[]>(() => { | ||
const activities: WebChatActivity[] = []; | ||
// Persistent Map to store activities by their keys | ||
const activityMapRef = useRef<Readonly<Map<string, WebChatActivity>>>( | ||
Object.freeze(new Map<string, WebChatActivity>()) | ||
); | ||
|
||
if (!activityKeys) { | ||
return rawActivities; | ||
} | ||
const activities = | ||
useReduceActivities<readonly WebChatActivity[]>((prevActivities = [], activity) => { | ||
if (!activityKeys) { | ||
return rawActivities; | ||
} | ||
|
||
for (const activity of rawActivities) { | ||
// If an activity has multiple revisions, display the latest revision only at the position of the first revision. | ||
const activityKey = getKeyByActivity(activity); | ||
|
||
// "Activities with same key" means "multiple revisions of same activity." | ||
const activitiesWithSameKey = getActivitiesByKey(getKeyByActivity(activity)); | ||
const activitiesWithSameKey = getActivitiesByKey(activityKey); | ||
|
||
// TODO: We may want to send all revisions of activity to the middleware so they can render UI to see previous revisions. | ||
activitiesWithSameKey?.[0] === activity && | ||
activities.push(activitiesWithSameKey[activitiesWithSameKey.length - 1]); | ||
} | ||
if (activitiesWithSameKey?.[activitiesWithSameKey.length - 1] === activity) { | ||
const activityMap = activityMapRef.current; | ||
|
||
// Update or add the activity in the persistent Map | ||
activityMap.set(activityKey, activity); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the |
||
|
||
// Return the updated activities as an array | ||
return Array.from(activityMap.values()); | ||
} | ||
|
||
return Object.freeze(activities); | ||
}, [activityKeys, getActivitiesByKey, getKeyByActivity, rawActivities]); | ||
return prevActivities; | ||
}) || []; | ||
|
||
const createActivityRenderer: ActivityComponentFactory = useCreateActivityRenderer(); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sort.