Skip to content
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
24 changes: 24 additions & 0 deletions src/cli/repl/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import type { FlowrConfigOptions } from '../../config';
import { genericWrapReplFailIfNoRequest, SupportedQueries, type SupportedQuery } from '../../queries/query';
import type { FlowrAnalyzer } from '../../project/flowr-analyzer';
import { startAndEndsWith } from '../../util/text/strings';
import type { RType } from '../../r-bridge/lang-4.x/ast/model/type';
import { instrumentDataflowCount } from '../../dataflow/instrument/instrument-dataflow-count';

let _replCompleterKeywords: string[] | undefined = undefined;
function replCompleterKeywords() {
Expand Down Expand Up @@ -128,6 +130,10 @@ export function handleString(code: string) {

async function replProcessStatement(output: ReplOutput, statement: string, analyzer: FlowrAnalyzer, allowRSessionAccess: boolean): Promise<void> {
const time = Date.now();
const heatMap = new Map<RType, number>();
if(analyzer.inspectContext().config.repl.dfProcessorHeat) {
analyzer.context().config.solver.instrument.dataflowExtractors = instrumentDataflowCount(heatMap, map => map.clear());
}
if(statement.startsWith(':')) {
const command = statement.slice(1).split(' ')[0].toLowerCase();
const processor = getCommand(command);
Expand Down Expand Up @@ -179,6 +185,24 @@ async function replProcessStatement(output: ReplOutput, statement: string, analy
// do nothing, this is just a nice-to-have
}
}
if(heatMap.size > 0 && analyzer.inspectContext().config.repl.dfProcessorHeat) {
const sorted = Array.from(heatMap.entries()).sort((a, b) => b[1] - a[1]);
console.log(output.formatter.format('[REPL Stats] Dataflow Processor Heatmap:', {
style: FontStyles.Italic,
effect: ColorEffect.Foreground,
color: Colors.White
}));
const longestKey = Math.max(...Array.from(heatMap.keys(), k => k.length));
const longestValue = Math.max(...Array.from(heatMap.values(), v => v.toString().length));
for(const [rType, count] of sorted) {
console.log(output.formatter.format(` - ${(rType + ':').padEnd(longestKey + 1, ' ')} ${count.toString().padStart(longestValue, ' ')}`, {
style: FontStyles.Italic,
effect: ColorEffect.Foreground,
color: Colors.White
}));
}
}

}

/**
Expand Down
28 changes: 25 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import Joi from 'joi';
import type { BuiltInDefinitions } from './dataflow/environments/built-in-config';
import type { KnownParser } from './r-bridge/parser';
import type { DeepWritable } from 'ts-essentials';
import type { DataflowProcessors } from './dataflow/processor';
import type { ParentInformation } from './r-bridge/lang-4.x/ast/model/processing/decorate';
import type { FlowrAnalyzerContext } from './project/context/flowr-analyzer-context';

export enum VariableResolve {
/** Don't resolve constants at all */
Expand Down Expand Up @@ -110,7 +113,9 @@ export interface FlowrConfigOptions extends MergeableRecord {
/** Configuration options for the REPL */
readonly repl: {
/** Whether to show quick stats in the REPL after each evaluation */
quickStats: boolean
quickStats: boolean
/** This instruments the dataflow processors to count how often each processor is called */
dfProcessorHeat: boolean;
}
readonly project: {
/** Whether to resolve unknown paths loaded by the r project disk when trying to source/analyze files */
Expand Down Expand Up @@ -146,6 +151,15 @@ export interface FlowrConfigOptions extends MergeableRecord {
*/
readonly maxIndexCount: number
},
/** These keys are only intended for use within code, allowing to instrument the dataflow analyzer! */
readonly instrument: {
/**
* Modify the dataflow processors used during dataflow analysis.
* Make sure that all processors required for correct analysis are still present!
* This may have arbitrary consequences on the analysis precision and performance, consider focusing on decorating existing processors instead of replacing them.
*/
dataflowExtractors?: (extractor: DataflowProcessors<ParentInformation>, ctx: FlowrAnalyzerContext) => DataflowProcessors<ParentInformation>
},
/**
* If lax source calls are active, flowR searches for sourced files much more freely,
* based on the configurations you give it.
Expand Down Expand Up @@ -238,7 +252,8 @@ export const defaultConfigOptions: FlowrConfigOptions = {
}
},
repl: {
quickStats: false
quickStats: false,
dfProcessorHeat: false
},
project: {
resolveUnknownPathsOnDisk: true
Expand All @@ -256,6 +271,9 @@ export const defaultConfigOptions: FlowrConfigOptions = {
searchPath: [],
repeatedSourceLimit: 2
},
instrument: {
dataflowExtractors: undefined
},
slicer: {
threshold: 50
}
Expand Down Expand Up @@ -283,7 +301,8 @@ export const flowrConfigFileSchema = Joi.object({
}).optional().description('Semantics regarding how to handle the R environment.')
}).description('Configure language semantics and how flowR handles them.'),
repl: Joi.object({
quickStats: Joi.boolean().optional().description('Whether to show quick stats in the REPL after each evaluation.')
quickStats: Joi.boolean().optional().description('Whether to show quick stats in the REPL after each evaluation.'),
dfProcessorHeat: Joi.boolean().optional().description('This instruments the dataflow processors to count how often each processor is called.')
}).description('Configuration options for the REPL.'),
project: Joi.object({
resolveUnknownPathsOnDisk: Joi.boolean().optional().description('Whether to resolve unknown paths loaded by the r project disk when trying to source/analyze files.')
Expand All @@ -310,6 +329,9 @@ export const flowrConfigFileSchema = Joi.object({
maxIndexCount: Joi.number().required().description('The maximum number of indices tracked per object with the pointer analysis.')
})
).description('Whether to track pointers in the dataflow graph, if not, the graph will be over-approximated wrt. containers and accesses.'),
instrument: Joi.object({
dataflowExtractors: Joi.any().optional().description('These keys are only intended for use within code, allowing to instrument the dataflow analyzer!')
}),
resolveSource: Joi.object({
dropPaths: Joi.string().valid(...Object.values(DropPathsOption)).description('Allow to drop the first or all parts of the sourced path, if it is relative.'),
ignoreCapitalization: Joi.boolean().description('Search for filenames matching in the lowercase.'),
Expand Down
4 changes: 2 additions & 2 deletions src/dataflow/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function resolveLinkToSideEffects(ast: NormalizedAst, graph: DataflowGraph) {
continue;
}
/* this has to change whenever we add a new link to relations because we currently offer no abstraction for the type */
const potentials = identifyLinkToLastCallRelation(s.id, cf.graph, graph, s.linkTo, knownCalls);
const potentials = identifyLinkToLastCallRelation(s.id, cf?.graph, graph, s.linkTo, knownCalls);
for(const pot of potentials) {
graph.addEdge(s.id, pot, EdgeType.Reads);
}
Expand Down Expand Up @@ -129,7 +129,7 @@ export function produceDataFlowGraph<OtherInfo>(
parser,
completeAst,
environment: ctx.env.makeCleanEnv(),
processors,
processors: ctx.config.solver.instrument.dataflowExtractors?.(processors, ctx) ?? processors,
controlDependencies: undefined,
referenceChain: [files[0].filePath],
ctx
Expand Down
25 changes: 25 additions & 0 deletions src/dataflow/instrument/instrument-dataflow-count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { DataflowProcessors } from '../processor';
import type { ParentInformation } from '../../r-bridge/lang-4.x/ast/model/processing/decorate';
import type { FlowrAnalyzerContext } from '../../project/context/flowr-analyzer-context';
import type { RType } from '../../r-bridge/lang-4.x/ast/model/type';
import type { RNode } from '../../r-bridge/lang-4.x/ast/model/model';
import type { DataflowInformation } from '../info';

/**
* This takes the out parameter `countMap` and fills it with the count of how many times each RType was processed.
* The accompanying `reset` function can be used to reset the map to an empty state.
*/
export function instrumentDataflowCount(countMap: Map<RType, number>, reset: (map: Map<RType, number>) => void): (extractor: DataflowProcessors<ParentInformation>, ctx: FlowrAnalyzerContext) => DataflowProcessors<ParentInformation> {
return (extractor, _ctx) => {
reset(countMap);
const instrumented: DataflowProcessors<ParentInformation> = {} as DataflowProcessors<ParentInformation>;
for(const [key, processor] of Object.entries(extractor) as [RType, (...args: unknown[]) => DataflowInformation][]) {
instrumented[key as RNode['type']] = ((...args: unknown[]) => {
const prev = countMap.get(key) ?? 0;
countMap.set(key, prev + 1);
return processor(...args);
}) as never;
}
return instrumented;
};
}
6 changes: 4 additions & 2 deletions src/documentation/wiki-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ ${codeBlock('json', JSON.stringify(
}
},
repl: {
quickStats: false
quickStats: false,
dfProcessorHeat: false
},
project: {
resolveUnknownPathsOnDisk: true
Expand All @@ -268,7 +269,8 @@ ${codeBlock('json', JSON.stringify(
inferWorkingDirectory: InferWorkingDirectory.ActiveScript,
searchPath: []
},
slicer: {
instrument: {},
slicer: {
threshold: 50
}
},
Expand Down
2 changes: 1 addition & 1 deletion src/documentation/wiki-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ ${
}

One of the most useful options to change on-the-fly are probably those under \`repl\`. For example, setting \`repl.quickStats=true\`
enables quick statistics after each REPL command.
enables quick statistics after each REPL command. Likewise, setting \`repl.dfProcessorHeat=true\` enables the dataflow processor heatmap after each REPL command.
`;
}
});
Expand Down
43 changes: 40 additions & 3 deletions src/queries/catalog/config-query/config-query-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Joi from 'joi';
import type { FlowrConfigOptions } from '../../../config';
import { jsonReplacer } from '../../../util/json';
import type { DeepPartial } from 'ts-essentials';
import type { ParsedQueryLine, SupportedQuery } from '../../query';
import type { ParsedQueryLine, Query, SupportedQuery } from '../../query';
import type { ReplOutput } from '../../../cli/repl/commands/repl-main';
import type { CommandCompletions } from '../../../cli/repl/core';

Expand Down Expand Up @@ -83,12 +83,49 @@ function configQueryLineParser(output: ReplOutput, line: readonly string[], _con
};
}

function collectKeysFromUpdate(update: DeepPartial<FlowrConfigOptions>, prefix: string = ''): string[] {
// only collect leaf keys
const keys: string[] = [];
for(const [key, value] of Object.entries(update)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if(value && typeof value === 'object' && !Array.isArray(value)) {
keys.push(...collectKeysFromUpdate(value as DeepPartial<FlowrConfigOptions>, fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}

function getValueAtPath(obj: object, path: string[]): unknown {
let current: unknown = obj;
for(const key of path) {
if(current && typeof current === 'object' && (current as Record<string, unknown>)[key] !== undefined) {
current = (current as Record<string, unknown>)[key];
} else {
return undefined;
}
}
return current;
}

export const ConfigQueryDefinition = {
executor: executeConfigQuery,
asciiSummarizer: (formatter: OutputFormatter, _analyzer: unknown, queryResults: BaseQueryResult, result: string[]) => {
asciiSummarizer: (formatter: OutputFormatter, _analyzer: unknown, queryResults: BaseQueryResult, result: string[], queries: readonly Query[]) => {
const out = queryResults as ConfigQueryResult;
result.push(`Query: ${bold('config', formatter)} (${printAsMs(out['.meta'].timing, 0)})`);
result.push(` ╰ Config:\n${JSON.stringify(out.config, jsonReplacer, 4)}`);
const configQueries = queries.filter(q => q.type === 'config');
if(configQueries.some(q => q.update)) {
const updatedKeys = configQueries.flatMap(q => q.update ? collectKeysFromUpdate(q.update) : []);
result.push(' ╰ Updated configuration:');
for(const key of updatedKeys) {
const path = key.split('.');
const newValue = getValueAtPath(out.config, path);
result.push(` - ${key}: ${JSON.stringify(newValue, jsonReplacer)}`);
}
} else {
result.push(` ╰ Config:\n${JSON.stringify(out.config, jsonReplacer, 4)}`);
}
return true;
},
completer: configReplCompleter,
Expand Down
Loading