perf: coalesce project chat realtime reload bursts
This commit is contained in:
@@ -85,6 +85,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private final ExecutorService replyWaitExecutor = Executors.newSingleThreadExecutor();
|
||||
private @Nullable BossRealtimeClient realtimeClient;
|
||||
private final java.util.HashMap<String, Long> recentRealtimeEventTimestamps = new java.util.HashMap<>();
|
||||
private boolean reloadInFlight;
|
||||
private boolean pendingReload;
|
||||
private boolean pendingReloadForcedScrollToBottom;
|
||||
|
||||
static final class ChromeBindings {
|
||||
final boolean multiSelecting;
|
||||
@@ -123,7 +126,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ProjectSnapshot {
|
||||
static final class ProjectSnapshot {
|
||||
final JSONObject payload;
|
||||
final @Nullable JSONArray dispatchPlans;
|
||||
final @Nullable JSONObject participantsPayload;
|
||||
@@ -331,28 +334,61 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (reloadInFlight) {
|
||||
pendingReload = true;
|
||||
pendingReloadForcedScrollToBottom = pendingReloadForcedScrollToBottom || forcedScrollToBottom;
|
||||
return;
|
||||
}
|
||||
renderNearBottom = isChatNearBottom();
|
||||
renderForcedScrollToBottom = forcedScrollToBottom;
|
||||
reloadInFlight = true;
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
ProjectSnapshot snapshot = fetchProjectSnapshot();
|
||||
runOnUiThread(() -> renderProject(snapshot.payload, snapshot.dispatchPlans, snapshot.participantsPayload));
|
||||
ProjectSnapshot snapshot = loadProjectSnapshotForRefresh();
|
||||
runOnUiThread(() -> {
|
||||
renderLoadedProjectSnapshot(snapshot);
|
||||
finishReloadCycle();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
composerSending = false;
|
||||
updateComposerSendButtonState();
|
||||
if (pendingOutgoingBubble == null && !masterAgentReplyWaiting) {
|
||||
replaceContent(BossUi.buildEmptyCard(this, "项目详情加载失败:" + error.getMessage()));
|
||||
} else {
|
||||
showMessage("项目详情刷新失败:" + error.getMessage());
|
||||
}
|
||||
handleProjectReloadFailure(error);
|
||||
finishReloadCycle();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ProjectSnapshot loadProjectSnapshotForRefresh() throws Exception {
|
||||
return fetchProjectSnapshot();
|
||||
}
|
||||
|
||||
void renderLoadedProjectSnapshot(ProjectSnapshot snapshot) {
|
||||
renderProject(snapshot.payload, snapshot.dispatchPlans, snapshot.participantsPayload);
|
||||
}
|
||||
|
||||
void handleProjectReloadFailure(Exception error) {
|
||||
setRefreshing(false);
|
||||
composerSending = false;
|
||||
updateComposerSendButtonState();
|
||||
if (pendingOutgoingBubble == null && !masterAgentReplyWaiting) {
|
||||
replaceContent(BossUi.buildEmptyCard(this, "项目详情加载失败:" + error.getMessage()));
|
||||
} else {
|
||||
showMessage("项目详情刷新失败:" + error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void finishReloadCycle() {
|
||||
reloadInFlight = false;
|
||||
if (!pendingReload) {
|
||||
return;
|
||||
}
|
||||
boolean forcedScrollToBottom = pendingReloadForcedScrollToBottom;
|
||||
pendingReload = false;
|
||||
pendingReloadForcedScrollToBottom = false;
|
||||
reload(forcedScrollToBottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setRefreshing(boolean refreshing) {
|
||||
super.setRefreshing(refreshing);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Looper;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -13,6 +17,10 @@ import org.robolectric.Shadows;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class ProjectDetailActivityRealtimeTest {
|
||||
@@ -164,17 +172,126 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
assertEquals(1, activity.reloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void burstRealtimeEventsWhileReloadingCoalesceIntoSingleFollowUpReload() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线");
|
||||
TestRealtimeProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestRealtimeProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.resume()
|
||||
.get();
|
||||
|
||||
activity.blockFirstReload();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-1"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
assertTrue(activity.awaitFirstLoadStarted());
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent("conversation.updated", new JSONObject().put("projectId", "project-1"))
|
||||
)
|
||||
);
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
ReflectionHelpers.ClassParameter.from(
|
||||
BossRealtimeEvent.class,
|
||||
new BossRealtimeEvent("master_agent.task.updated", new JSONObject().put("projectId", "project-1"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, activity.loadCallCount);
|
||||
assertEquals(0, activity.renderCount);
|
||||
|
||||
activity.releaseFirstLoad();
|
||||
waitFor(() -> activity.renderCount == 2 && activity.loadCallCount == 2);
|
||||
|
||||
assertEquals(2, activity.loadCallCount);
|
||||
assertEquals(2, activity.renderCount);
|
||||
}
|
||||
|
||||
private static void waitFor(BooleanSupplier condition) throws Exception {
|
||||
long deadlineAt = System.currentTimeMillis() + 2_000L;
|
||||
while (System.currentTimeMillis() < deadlineAt) {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||||
if (condition.getAsBoolean()) {
|
||||
return;
|
||||
}
|
||||
Thread.sleep(20L);
|
||||
}
|
||||
fail("condition not met before timeout");
|
||||
}
|
||||
|
||||
public static class TestRealtimeProjectDetailActivity extends ProjectDetailActivity {
|
||||
int reloadCount;
|
||||
volatile int loadCallCount;
|
||||
volatile int renderCount;
|
||||
private CountDownLatch firstLoadStarted;
|
||||
private CountDownLatch releaseFirstLoad;
|
||||
|
||||
@Override
|
||||
boolean shouldLoadOnCreate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void blockFirstReload() {
|
||||
firstLoadStarted = new CountDownLatch(1);
|
||||
releaseFirstLoad = new CountDownLatch(1);
|
||||
}
|
||||
|
||||
boolean awaitFirstLoadStarted() throws InterruptedException {
|
||||
return firstLoadStarted != null && firstLoadStarted.await(2, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
void releaseFirstLoad() {
|
||||
if (releaseFirstLoad != null) {
|
||||
releaseFirstLoad.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
reloadCount += 1;
|
||||
super.reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
ProjectSnapshot loadProjectSnapshotForRefresh() throws Exception {
|
||||
loadCallCount += 1;
|
||||
if (loadCallCount == 1 && firstLoadStarted != null && releaseFirstLoad != null) {
|
||||
firstLoadStarted.countDown();
|
||||
releaseFirstLoad.await(2, TimeUnit.SECONDS);
|
||||
}
|
||||
return new ProjectSnapshot(
|
||||
new JSONObject().put(
|
||||
"project",
|
||||
new JSONObject()
|
||||
.put("name", "北区试产线")
|
||||
.put("messages", new JSONArray())
|
||||
),
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
void renderLoadedProjectSnapshot(ProjectSnapshot snapshot) {
|
||||
renderCount += 1;
|
||||
setRefreshing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user