Skip to content

Commit d672c12

Browse files
authored
Add toggle to show only changed columns in experiments table (#4402)
* add show only changed columns to experiments table * persist show only changed between sessions * add experiment webview tests * add experiment webview tests * add unit tests for changed column collection * refactor collect columns with changed values * add disable only changed button to get started screen * address review comments
1 parent d54107e commit d672c12

File tree

22 files changed

+322
-24
lines changed

22 files changed

+322
-24
lines changed

extension/src/experiments/columns/collect/index.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
/* eslint-disable sort-keys-fix/sort-keys-fix */
2-
import { collectChanges, collectColumns, collectRelativeMetricsFiles } from '.'
2+
import {
3+
collectChanges,
4+
collectColumns,
5+
collectColumnsWithChangedValues,
6+
collectRelativeMetricsFiles
7+
} from '.'
38
import { timestampColumn } from '../constants'
49
import { buildMetricOrParamPath } from '../paths'
510
import { ColumnType } from '../../webview/contract'
@@ -15,6 +20,8 @@ import {
1520
} from '../../../cli/dvc/contract'
1621
import { getConfigValue } from '../../../vscode/config'
1722
import { generateTestExpShowOutput } from '../../../test/util/experiments'
23+
import rowsFixture from '../../../test/fixtures/expShow/base/rows'
24+
import { Operator, filterExperiment } from '../../model/filterBy'
1825

1926
jest.mock('../../../vscode/config')
2027

@@ -525,3 +532,54 @@ describe('collectRelativeMetricsFiles', () => {
525532
expect(collectRelativeMetricsFiles([])).toStrictEqual(existingFiles)
526533
})
527534
})
535+
536+
describe('collectColumnsWithChangedValues', () => {
537+
it('should return the expected columns from the test fixture', () => {
538+
const changedColumns = collectColumnsWithChangedValues(
539+
columnsFixture,
540+
rowsFixture,
541+
[]
542+
)
543+
expect(changedColumns).toStrictEqual(
544+
columnsFixture.filter(({ path }) =>
545+
[
546+
'metrics:summary.json',
547+
'metrics:summary.json:loss',
548+
'metrics:summary.json:accuracy',
549+
'metrics:summary.json:val_loss',
550+
'metrics:summary.json:val_accuracy',
551+
'params:params.yaml',
552+
'params:params.yaml:code_names',
553+
'params:params.yaml:epochs',
554+
'params:params.yaml:learning_rate',
555+
'params:params.yaml:dropout',
556+
'params:params.yaml:process',
557+
'params:params.yaml:process.threshold',
558+
'params:params.yaml:process.test_arg'
559+
].includes(path)
560+
)
561+
)
562+
})
563+
564+
it('should return the expected columns when applying filters (calculate changed after filters)', () => {
565+
const uniformColumn = 'params:params.yaml:dropout'
566+
const filters = [
567+
{
568+
path: uniformColumn,
569+
operator: Operator.EQUAL,
570+
value: 0.124
571+
}
572+
]
573+
const filteredRows = [...rowsFixture]
574+
for (const row of filteredRows) {
575+
row.subRows = row.subRows?.filter(exp => filterExperiment(filters, exp))
576+
}
577+
578+
const changedColumns = collectColumnsWithChangedValues(
579+
columnsFixture,
580+
filteredRows,
581+
filters
582+
)
583+
expect(changedColumns.map(({ path }) => path)).not.toContain(uniformColumn)
584+
})
585+
})

extension/src/experiments/columns/collect/index.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import { join } from 'path'
2+
import get from 'lodash.get'
23
import isEqual from 'lodash.isequal'
34
import { ColumnAccumulator } from './util'
45
import { collectDepChanges, collectDeps } from './deps'
56
import {
67
collectMetricAndParamChanges,
78
collectMetricsAndParams
89
} from './metricsAndParams'
9-
import { Column } from '../../webview/contract'
10+
import { Column, Commit, Experiment } from '../../webview/contract'
1011
import {
1112
ExpRange,
1213
ExpShowOutput,
1314
ExpState,
1415
ExpData,
15-
experimentHasError
16+
experimentHasError,
17+
Value,
18+
EXPERIMENT_WORKSPACE_ID
1619
} from '../../../cli/dvc/contract'
1720
import { standardizePath } from '../../../fileSystem/path'
1821
import { timestampColumn } from '../constants'
1922
import { sortCollectedArray, uniqueValues } from '../../../util/array'
23+
import { FilterDefinition, filterExperiment } from '../../model/filterBy'
2024

2125
const collectFromExperiment = (
2226
acc: ColumnAccumulator,
@@ -136,3 +140,69 @@ export const collectRelativeMetricsFiles = (
136140

137141
return uniqueValues(files)
138142
}
143+
144+
const getValue = (
145+
experiment: Commit | Experiment,
146+
pathArray: string[]
147+
): Value => get(experiment, pathArray) as Value
148+
149+
const collectChangedPath = (
150+
acc: string[],
151+
path: string,
152+
pathArray: string[],
153+
records: (Commit | Experiment)[]
154+
) => {
155+
let initialValue
156+
for (const experiment of records) {
157+
if (initialValue === undefined) {
158+
initialValue = getValue(experiment, pathArray)
159+
continue
160+
}
161+
const value = getValue(experiment, pathArray)
162+
if (value === undefined || isEqual(value, initialValue)) {
163+
continue
164+
}
165+
166+
acc.push(path)
167+
return
168+
}
169+
}
170+
171+
const collectChangedPaths = (
172+
columns: Column[],
173+
records: (Commit | Experiment)[]
174+
) => {
175+
const acc: string[] = []
176+
for (const { pathArray, path, hasChildren } of columns) {
177+
if (!pathArray || hasChildren) {
178+
continue
179+
}
180+
collectChangedPath(acc, path, pathArray, records)
181+
}
182+
return acc
183+
}
184+
185+
export const collectColumnsWithChangedValues = (
186+
selectedColumns: Column[],
187+
rows: Commit[],
188+
filters: FilterDefinition[]
189+
): Column[] => {
190+
const records = []
191+
for (const commit of rows) {
192+
if (
193+
commit.id === EXPERIMENT_WORKSPACE_ID ||
194+
filterExperiment(filters, commit as Experiment)
195+
) {
196+
records.push(commit)
197+
}
198+
if (commit.subRows) {
199+
records.push(...commit.subRows)
200+
}
201+
}
202+
203+
const changedPaths = collectChangedPaths(selectedColumns, records)
204+
205+
return selectedColumns.filter(({ path }) =>
206+
changedPaths.find(changedPath => changedPath.startsWith(path))
207+
)
208+
}

extension/src/experiments/columns/model.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export class ColumnsModel extends PathSelectionModel<Column> {
2323
private columnsChanges: string[] = []
2424
private paramsFiles = new Set<string>()
2525
private relativeMetricsFiles: string[] = []
26+
private showOnlyChanged: boolean
2627

2728
constructor(
2829
dvcRoot: string,
@@ -44,6 +45,7 @@ export class ColumnsModel extends PathSelectionModel<Column> {
4445
PersistenceKey.METRICS_AND_PARAMS_COLUMN_WIDTHS,
4546
{}
4647
)
48+
this.showOnlyChanged = this.revive(PersistenceKey.SHOW_ONLY_CHANGED, false)
4749
}
4850

4951
public getColumnOrder(): string[] {
@@ -119,6 +121,15 @@ export class ColumnsModel extends PathSelectionModel<Column> {
119121
)
120122
}
121123

124+
public getShowOnlyChanged() {
125+
return this.showOnlyChanged
126+
}
127+
128+
public toggleShowOnlyChanged() {
129+
this.showOnlyChanged = !this.showOnlyChanged
130+
this.persist(PersistenceKey.SHOW_ONLY_CHANGED, this.showOnlyChanged)
131+
}
132+
122133
public getChildren(path: string | undefined) {
123134
return this.filterChildren(path).map(element => {
124135
return {

extension/src/experiments/model/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,6 @@ export class ExperimentsModel extends ModelWithPersistence {
246246
return [...this.filters.values()]
247247
}
248248

249-
public getFilterPaths() {
250-
return this.getFilters().map(({ path }) => path)
251-
}
252-
253249
public addFilter(filter: FilterDefinition) {
254250
this.filters.set(getFilterId(filter), filter)
255251
this.applyAndPersistFilters()

extension/src/experiments/webview/contract.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export type TableData = {
109109
hasRunningWorkspaceExperiment: boolean
110110
isShowingMoreCommits: Record<string, boolean>
111111
rows: Commit[]
112+
showOnlyChanged: boolean
112113
selectedBranches: string[]
113114
selectedForPlotsCount: number
114115
sorts: SortDefinition[]

extension/src/experiments/webview/messages.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Title } from '../../vscode/title'
2222
import { ConfigKey, setConfigValue } from '../../vscode/config'
2323
import { NUM_OF_COMMITS_TO_INCREASE } from '../../cli/dvc/constants'
2424
import { Pipeline } from '../../pipeline'
25+
import { collectColumnsWithChangedValues } from '../columns/collect'
2526

2627
export class WebviewMessages {
2728
private readonly dvcRoot: string
@@ -201,6 +202,9 @@ export class WebviewMessages {
201202
case MessageFromWebviewType.REFRESH_EXP_DATA:
202203
return this.refreshData()
203204

205+
case MessageFromWebviewType.TOGGLE_SHOW_ONLY_CHANGED:
206+
return this.toggleShowOnlyChanged()
207+
204208
default:
205209
Logger.error(`Unexpected message: ${JSON.stringify(message)}`)
206210
}
@@ -258,14 +262,34 @@ export class WebviewMessages {
258262
return this.update()
259263
}
260264

265+
private toggleShowOnlyChanged() {
266+
this.columns.toggleShowOnlyChanged()
267+
this.sendWebviewMessage()
268+
sendTelemetryEvent(
269+
EventName.VIEWS_EXPERIMENTS_TABLE_REFRESH,
270+
undefined,
271+
undefined
272+
)
273+
}
274+
261275
private getWebviewData(): TableData {
276+
const filters = this.experiments.getFilters()
277+
const rows = this.experiments.getRowData()
278+
const selectedColumns = this.columns.getSelected()
279+
280+
const showOnlyChanged = this.columns.getShowOnlyChanged()
281+
282+
const columns = showOnlyChanged
283+
? collectColumnsWithChangedValues(selectedColumns, rows, filters)
284+
: selectedColumns
285+
262286
return {
263287
changes: this.columns.getChanges(),
264288
cliError: this.experiments.getCliError() || null,
265289
columnOrder: this.columns.getColumnOrder(),
266290
columnWidths: this.columns.getColumnWidths(),
267-
columns: this.columns.getSelected(),
268-
filters: this.experiments.getFilterPaths(),
291+
columns,
292+
filters: filters.map(({ path }) => path),
269293
hasBranchesToSelect:
270294
this.experiments.getAvailableBranchesToShow().length > 0,
271295
hasCheckpoints: this.experiments.hasCheckpoints(),
@@ -275,9 +299,10 @@ export class WebviewMessages {
275299
hasRunningWorkspaceExperiment:
276300
this.experiments.hasRunningWorkspaceExperiment(),
277301
isShowingMoreCommits: this.experiments.getIsShowingMoreCommits(),
278-
rows: this.experiments.getRowData(),
302+
rows,
279303
selectedBranches: this.experiments.getSelectedBranches(),
280304
selectedForPlotsCount: this.experiments.getSelectedRevisions().length,
305+
showOnlyChanged,
281306
sorts: this.experiments.getSorts()
282307
}
283308
}

extension/src/persistence/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export enum PersistenceKey {
1818
PLOT_SECTION_COLLAPSED = 'plotSectionCollapsed:',
1919
PLOT_SELECTED_METRICS = 'plotSelectedMetrics:',
2020
PLOTS_SMOOTH_PLOT_VALUES = 'plotSmoothPlotValues:',
21-
PLOT_TEMPLATE_ORDER = 'plotTemplateOrder:'
21+
PLOT_TEMPLATE_ORDER = 'plotTemplateOrder:',
22+
SHOW_ONLY_CHANGED = 'columnsShowOnlyChanged:'
2223
}
2324

2425
export enum GlobalPersistenceKey {

extension/src/telemetry/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export const EventName = Object.assign(
7272
'views.experimentsTable.showMoreCommits',
7373
VIEWS_EXPERIMENTS_TABLE_SORT_COLUMN:
7474
'views.experimentsTable.columnSortAdded',
75+
VIEWS_EXPERIMENTS_TABLE_TOGGLE_SHOW_ONLY_CHANGED:
76+
'views.experimentsTable.toggleShowOnlyChanged',
7577

7678
VIEWS_PLOTS_CLOSED: 'views.plots.closed',
7779
VIEWS_PLOTS_COMPARISON_ROWS_REORDERED:
@@ -266,6 +268,7 @@ export interface IEventNamePropertyMapping {
266268
[EventName.VIEWS_EXPERIMENTS_TABLE_OPEN_PARAMS_FILE]: {
267269
path: string
268270
}
271+
[EventName.VIEWS_EXPERIMENTS_TABLE_TOGGLE_SHOW_ONLY_CHANGED]: undefined
269272

270273
[EventName.VIEWS_PLOTS_CLOSED]: undefined
271274
[EventName.VIEWS_PLOTS_CREATED]: undefined

extension/src/test/fixtures/expShow/base/tableData.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import rowsFixture from './rows'
33
import columnsFixture from './columns'
44

55
const tableDataFixture: TableData = {
6-
selectedBranches: [],
7-
cliError: null,
86
changes: [],
7+
cliError: null,
98
columnOrder: [],
109
columns: columnsFixture,
1110
columnWidths: {},
@@ -18,7 +17,9 @@ const tableDataFixture: TableData = {
1817
hasRunningWorkspaceExperiment: true,
1918
isShowingMoreCommits: { main: true },
2019
rows: rowsFixture,
20+
selectedBranches: [],
2121
selectedForPlotsCount: 2,
22+
showOnlyChanged: false,
2223
sorts: []
2324
}
2425

extension/src/test/suite/experiments/index.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,25 @@ suite('Experiments Test Suite', () => {
12971297
expect(mockShowPlots).to.be.calledWith(dvcDemoPath)
12981298
}).timeout(WEBVIEW_TEST_TIMEOUT)
12991299

1300+
it('should be able to handle a message to toggle only changed columns', async () => {
1301+
const { columnsModel, messageSpy, mockMessageReceived } =
1302+
await buildExperimentsWebview({
1303+
disposer: disposable
1304+
})
1305+
1306+
expect(columnsModel.getShowOnlyChanged()).to.be.false
1307+
messageSpy.resetHistory()
1308+
1309+
mockMessageReceived.fire({
1310+
type: MessageFromWebviewType.TOGGLE_SHOW_ONLY_CHANGED
1311+
})
1312+
1313+
expect(columnsModel.getShowOnlyChanged()).to.be.true
1314+
expect(messageSpy).to.be.calledWithMatch({
1315+
showOnlyChanged: true
1316+
})
1317+
}).timeout(WEBVIEW_TEST_TIMEOUT)
1318+
13001319
it('should handle a message to stop experiments running', async () => {
13011320
const { experiments, dvcExecutor } =
13021321
stubWorkspaceExperimentsGetters(disposable)

0 commit comments

Comments
 (0)