Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4b5eefb
Use the new dependency map for getDependencyTasks()/getBlockedTasks()
ashprice Jun 17, 2026
a9a92a9
Use the pending tasks cache in the circular dependency check.
ashprice Jun 17, 2026
9c70907
Use is_blocked for rendering report columns instead of copying whole …
ashprice Jun 17, 2026
6f34030
Use getDependencyUUIDs() to avoid copying full objects from the cache…
ashprice Jun 19, 2026
14d7f70
Use get_ref in ColDepends::modify()
ashprice Jun 19, 2026
cbe9540
Feat: Pre-parse urgency coefficients for reports.
ashprice Jun 19, 2026
359b993
Guard urgency() against infinite recursion.
ashprice Jul 17, 2026
fff5641
Add subset_indices() to filter pending tasks without copying.
ashprice Jul 17, 2026
4755351
Add +READY to pending-only filter checks.
ashprice Jul 17, 2026
8b3ad3e
Use const auto& for pending/completed tasks in subset()
ashprice Jul 17, 2026
9233d94
Use const in column measure/render/colorize functions; use get_ref fo…
ashprice Jul 17, 2026
5de5706
Switched to get_ref() inside many functions used for reports.
ashprice Jul 17, 2026
f841cc8
Merge branch 'GothenburgBitFactory:develop' into native-depmap
ashprice Jul 17, 2026
19543ca
Small fixes and comments
ashprice Aug 15, 2026
6a14fd7
Cleaned up implementation of previous commits.
ashprice Aug 15, 2026
c804683
Comments.
ashprice Aug 25, 2026
36841b5
Precompute duration values and UDA columns for sort
ashprice Aug 25, 2026
eb6cc81
Extend get_ref() approach to ColDescription and ColProject
ashprice Aug 25, 2026
f963136
Precompute sorted dependency field to avoid repeated getDependencyUUI…
ashprice Aug 25, 2026
498dca6
Use the pending tasks cache in the circular dependency check.
ashprice Aug 24, 2026
caf7d2b
avoid cache loads during bulk operations, update cache flags individu…
ashprice Aug 27, 2026
8d3c6ff
fixed regression: dependency id sorting
ashprice Aug 27, 2026
b1257c1
Fixed hooks regression + upstream bug
ashprice Aug 27, 2026
3cc792b
Waiting/random report and sorting micro-optimisations
ashprice Aug 27, 2026
3deda49
Syncing fork with upstream
ashprice Aug 27, 2026
e8a0de4
Tighten pendingOnly filter guard
ashprice Aug 27, 2026
909ebcf
Improve dependency flag handling; add tests
ashprice Aug 28, 2026
5d84093
Fixed regression: dep id listing with gc off; test added
ashprice Aug 28, 2026
5edd5b7
Skip clean working set rebuilds and invalidate caches after replica m…
ashprice Aug 28, 2026
d551358
Simplify the rendering of project column
ashprice Aug 28, 2026
3c3935b
Sync test doesn't really test what we want it to, removed
ashprice Aug 28, 2026
2ad4db7
Caching-related simplifications
ashprice Aug 28, 2026
3ccf8aa
Simplify sort comparison
ashprice Aug 28, 2026
6098997
Batch project feedback for bulk mods
ashprice Aug 31, 2026
55b82a0
Better handling of bulk updates to recurrence masks + tests
ashprice Aug 31, 2026
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
4 changes: 4 additions & 0 deletions src/Context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,10 @@ void Context::staticInitialization() {
for (auto& var : config.all())
if (var.substr(0, 13) == "urgency.user." || var.substr(0, 12) == "urgency.uda.")
Task::coefficients[var] = config.getReal(var);

// Pre-parse the coefficient keys, so urgency_c() doesn't have to re-parse for
// each task.
Task::setUrgencyCoefficients();
}

////////////////////////////////////////////////////////////////////////////////
Expand Down
190 changes: 90 additions & 100 deletions src/Filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,31 +44,7 @@ void Filter::subset(const std::vector<Task>& input, std::vector<Task>& output) {

Context::getContext().cli2.prepareFilter();

std::vector<std::pair<std::string, Lexer::Type>> precompiled;
for (auto& a : Context::getContext().cli2._args)
if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype);

if (precompiled.size()) {
Eval eval;
eval.addSource(domSource);

// Debug output from Eval during compilation is useful. During evaluation
// it is mostly noise.
eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3 ? true : false);
eval.compileExpression(precompiled);

for (auto& task : input) {
// Set up context for any DOM references.
auto currentTask = Context::getContext().withCurrentTask(&task);

Variant var;
eval.evaluateCompiledExpression(var);
if (var.get_bool()) output.push_back(task);
}

eval.debug(false);
} else
output = input;
filter_to_tasks(input, output);

_endCount = (int)output.size();
Context::getContext().debug(
Expand All @@ -86,51 +62,28 @@ void Filter::subset(std::vector<Task>& output) {
for (auto& a : Context::getContext().cli2._args)
if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype);

// Shortcut indicates that only pending.data needs to be loaded.
// Shortcut indicates that only tasks in the working set are loaded.
bool shortcut = false;

if (precompiled.size()) {
Timer timer_pending;
auto pending = Context::getContext().tdb2.pending_tasks();
const auto& pending = Context::getContext().tdb2.pending_tasks();
Context::getContext().time_filter_us -= timer_pending.total_us();
_startCount = (int)pending.size();

Eval eval;
eval.addSource(domSource);

// Debug output from Eval during compilation is useful. During evaluation
// it is mostly noise.
eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3 ? true : false);
eval.compileExpression(precompiled);

output.clear();
for (auto& task : pending) {
// Set up context for any DOM references.
auto currentTask = Context::getContext().withCurrentTask(&task);

Variant var;
eval.evaluateCompiledExpression(var);
if (var.get_bool()) output.push_back(task);
}
filter_to_tasks(pending, output);

shortcut = pendingOnly();
if (!shortcut) {
Timer timer_completed;
auto completed = Context::getContext().tdb2.completed_tasks();
const auto& completed = Context::getContext().tdb2.completed_tasks();
Context::getContext().time_filter_us -= timer_completed.total_us();
_startCount += (int)completed.size();

for (auto& task : completed) {
// Set up context for any DOM references.
auto currentTask = Context::getContext().withCurrentTask(&task);

Variant var;
eval.evaluateCompiledExpression(var);
if (var.get_bool()) output.push_back(task);
}
filter_to_tasks(completed, output);
}

eval.debug(false);
} else {
safety();

Expand All @@ -145,73 +98,110 @@ void Filter::subset(std::vector<Task>& output) {
Context::getContext().time_filter_us += timer.total_us();
}

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
bool Filter::hasFilter() const {
for (const auto& a : Context::getContext().cli2._args)
if (a.hasTag("FILTER")) return true;

return false;
}

/////////////////////////////////////////////////////////////////////////////////
// Evaluates a pre-parsed filter against a set of tasks and stores their indices
// from the vector. The filter is parsed with prepareFilter(), but this
// function does not call that or safety() itself - callers are expected to do so.
void Filter::filter_to_indices(const std::vector<Task>& pending, std::vector<int>& indices) const {
std::vector<std::pair<std::string, Lexer::Type>> precompiled;
for (auto& a : Context::getContext().cli2._args)
if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype);

if (precompiled.empty()) {
indices.reserve(pending.size());
for (int i = 0; i < (int)pending.size(); ++i) indices.push_back(i);
} else {
Eval eval;
eval.addSource(domSource);
eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3);
eval.compileExpression(precompiled);
for (int i = 0; i < (int)pending.size(); ++i) {
auto currentTask = Context::getContext().withCurrentTask(&pending[i]);
Variant var;
eval.evaluateCompiledExpression(var);
if (var.get_bool()) indices.push_back(i);
}
eval.debug(false);
}

Context::getContext().debug(
format("Filtered {1} tasks --> {2} tasks [pending only]", pending.size(), indices.size()));
}

////////////////////////////////////////////////////////////////////////////////
// Like filter_to_indices, but copies matched tasks into the output.
void Filter::filter_to_tasks(const std::vector<Task>& input, std::vector<Task>& output) const {
std::vector<std::pair<std::string, Lexer::Type>> precompiled;
for (auto& a : Context::getContext().cli2._args)
if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype);

if (precompiled.empty()) {
output = input;
} else {
Eval eval;
eval.addSource(domSource);
eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3);
eval.compileExpression(precompiled);
for (auto& task : input) {
auto currentTask = Context::getContext().withCurrentTask(&task);
Variant var;
eval.evaluateCompiledExpression(var);
if (var.get_bool()) output.push_back(task);
}
eval.debug(false);
}
}

////////////////////////////////////////////////////////////////////////////////
// If the filter contains no 'or', 'xor' or 'not' operators, and only includes
// status values 'pending', 'waiting' or 'recurring', then the filter is
// guaranteed to only need data from pending.data.

bool Filter::pendingOnly() const {
// When GC is off, there are no shortcuts.
if (!Context::getContext().config.getBoolean("gc")) return false;

// To skip loading completed.data, there should be:
// - 'status' in filter
// - no 'completed'
// - no 'deleted'
// - no 'xor'
// - no 'or'
int countStatus = 0;
int countPending = 0;
int countWaiting = 0;
int countRecurring = 0;
int countId = (int)Context::getContext().cli2._id_ranges.size();
int countUUID = (int)Context::getContext().cli2._uuid_list.size();
int countOr = 0;
int countXor = 0;
int countNot = 0;
bool pendingTag = false;
bool activeTag = false;

for (const auto& a : Context::getContext().cli2._args) {
if (a.hasTag("FILTER")) {
std::string raw = a.attribute("raw");
std::string canonical = a.attribute("canonical");

if (a._lextype == Lexer::Type::op && raw == "or") ++countOr;
if (a._lextype == Lexer::Type::op && raw == "xor") ++countXor;
if (a._lextype == Lexer::Type::op && raw == "not") ++countNot;
if (a._lextype == Lexer::Type::dom && canonical == "status") ++countStatus;
if (raw == "pending") ++countPending;
if (raw == "waiting") ++countWaiting;
if (raw == "recurring") ++countRecurring;
}
}
const auto& cli = Context::getContext().cli2;
if (!cli._uuid_list.empty()) return false;

for (const auto& word : Context::getContext().cli2._original_args) {
if (word.attribute("raw") == "+PENDING") pendingTag = true;
if (word.attribute("raw") == "+ACTIVE") activeTag = true;
}

if (countUUID) return false;
std::vector<const A2*> filter_args;
for (const auto& arg : cli._args) {
if (!arg.hasTag("FILTER")) continue;

if (countOr || countXor || countNot) return false;
const auto& raw = arg.attribute("raw");
if (arg._lextype == Lexer::Type::op &&
(raw == "or" || raw == "xor" || raw == "!" || raw == "not"))
return false;

if (pendingTag || activeTag) return true;

if (countStatus) {
if (!countPending && !countWaiting && !countRecurring) return false;
filter_args.push_back(&arg);
}

return true;
for (size_t i = 0; i + 2 < filter_args.size(); ++i) {
const auto& left = *filter_args[i];
const auto& op = *filter_args[i + 1];
const auto& right = *filter_args[i + 2];
const auto& value = right.attribute("raw");

if (left._lextype == Lexer::Type::dom && left.attribute("canonical") == "status" &&
op._lextype == Lexer::Type::op &&
(op.attribute("raw") == "=" || op.attribute("raw") == "==") &&
(value == "pending" || value == "waiting" || value == "recurring"))
return true;

if (left._lextype == Lexer::Type::dom && left.attribute("raw") == "tags" &&
op._lextype == Lexer::Type::op && op.attribute("raw") == "_hastag_" &&
(value == "PENDING" || value == "ACTIVE" || value == "READY" || value == "WAITING"))
return true;
}

if (countId) return true;
if (!cli._id_ranges.empty()) return true;

return false;
}
Expand Down
7 changes: 7 additions & 0 deletions src/Filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ class Filter {
void subset(std::vector<Task>&);
bool hasFilter() const;
bool pendingOnly() const;

// These evaluate already-prepared filters (ie. caller calls
// cli2.prepareFilter() and safety()). filter_to_indices() stores matching
// indices, filter_to_tasks() copies task objects.
void filter_to_indices(const std::vector<Task>&, std::vector<int>&) const;
void filter_to_tasks(const std::vector<Task>&, std::vector<Task>&) const;

void safety() const;
void disableSafety();

Expand Down
9 changes: 9 additions & 0 deletions src/Hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@ void Hooks::onModify(Task& before, Task& after) const {
if (!_enabled) return;

Timer timer;
const auto id = after.id;
const auto is_blocked = after.is_blocked;
const auto is_blocking = after.is_blocking;

std::vector<std::string> matchingScripts = scripts("on-modify");
if (matchingScripts.size()) {
Expand Down Expand Up @@ -314,6 +317,9 @@ void Hooks::onModify(Task& before, Task& after) const {
}

after = Task(input[1]);
after.id = id;
after.is_blocked = is_blocked;
after.is_blocking = is_blocking;
}

Context::getContext().time_hooks_us += timer.total_us();
Expand All @@ -322,6 +328,9 @@ void Hooks::onModify(Task& before, Task& after) const {
////////////////////////////////////////////////////////////////////////////////
std::vector<std::string> Hooks::list() const { return _scripts; }

////////////////////////////////////////////////////////////////////////////////
bool Hooks::hasOnModify() const { return _enabled && !scripts("on-modify").empty(); }

////////////////////////////////////////////////////////////////////////////////
std::vector<std::string> Hooks::scripts(const std::string& event) const {
std::vector<std::string> matching;
Expand Down
1 change: 1 addition & 0 deletions src/Hooks.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class Hooks {
void onExit() const;
void onAdd(Task&) const;
void onModify(Task&, Task&) const;
bool hasOnModify() const;
std::vector<std::string> list() const;

private:
Expand Down
Loading
Loading