Logging in any large multithreaded software is more complicated than one might initially think.
Widgets may want to display overall busyness status, while multiple actions are happening at once. Processes that track their progress may be interrupted or cancelled.
Another problem arises from functions that call other functions that also want to report progress. How can a function report its progress independently and have it automatically added in?
Clearly the reporting of the actions and what to do with that information must be abstracted away. But by doing so, we might limit our ability to control certain streams.
Even if we could accomplish the above, would it be so complicated that programmers forgo adding it to simple functions?
– Easily allow for optional, dummy-proof logging across all functions. Must be lightning fast an no-op if nothing listening.
– Easily handle recursion such that smaller functions can set progress of part of a larger without any knowledge of the larger process.
– Handle the ability to log to multiple locations and targets in an abstract way.
– Handle multithreading
The NDEVR solves this problem by breaking the logic into two seperate, simple interfaces:
The Complicated logic can then be abstracted away and handled by the API, freeing consumers of these classes to use them as simply as they prefer.
For functions that benefit from logging, NDEVR uses the concept of an Information Pipe in the form of InfoPipe. Processes can optionally log their progress in a few ways.
The simplest form is to simply log Application Messages to the InfoPipe directly:
//if default parameter used, all logging is no-op.
void doSomething(PipePtr log = PipePtr())
{
log.addMessage("This is a message");
[...]
log.addMessage(_t("This is a translated message"));
[...]
log.addMessage("warning", LogMessage::e_warning);
}
Now there are two ways to call the above function, one where we log information, and the other where we don’t:
Log* log = new Log();//A basic form of a Pipe
doSomething(log);//PipePtr will log information
doSomething();//PipePtr will NOT log information
For processes that track completion percentage, the ProgressInfo class can be used to automatically add percentage to an application, without any need to understand the overall process.
//progress info no-op if default argument used.
void doSomething(PipePtr log = PipePtr())
{
ProgressInfo progress(_t("Task Name"), log);
for(uint04 i = 0; i < 100; i++)
{
//one way to log
progress.setProgress(i, 100);
//another way
progress.setProgress(cast(i)/100.0f);
progress.addMessage("Also add messages");
[...]
}
//No need to clean up any resources
}
Nested logic is also very easy to manage. The callee does not need to know how they fit into some potential massive progress logic, but needs to only track their progress.
//progress info no-op if default argument used.
void doABigSomething(PipePtr log = PipePtr())
{
ProgressInfo progress(_t("Task Name"), log);
for(uint04 i = 0; i < 20; i++)
{
progress.setProgress(i, 100);
[...]
}
//We can use the createSubLog to augment how child
//logic is written. Below, all ProgressInfo will have
//be automatically adjusted such that 0% = 20% and 100% = 60%.
//Note this logic works recursively so we may already
//have a modifier applied when using this function!
//Remember doSomething else only takes in an optional log!
doSomething(progress.createSubLog(0.2, 0.4));
//now we do the rest of our function
for(uint04 i = 60; i < 100; i++)
{
progress.setProgress(i, 100);
[...]
}
}
For processes that support Cancelling the CancelInfo class can be take an InfoPipe and inform it of the ability to cancel, while also automatically clearing this information on destruction. You can check whether or not a cancel has been requested using the cancelRequested() method.
//Cancel always returns false if default arguments used.
bool doSomething(PipePtr log = PipePtr())
{
CancelInfo cancel(_t("Task Name"), log);
for(uint04 i = 0; i < 100; i++)
{
if(cancel.cancelRequested())
{
log.addMessage("exiting early");
return false;
}
}
log.addMessage("process completed");
return true;
}
Finally, for streams that allow for input interaction, a basic class InputInfoPipe allows for optional input. Note this should not be used for complex interaction. The best use case is for Pipelines that communicate with external applications.
//Cancel always returns false if default arguments used.
void printASentence(PipePtr log)
{
lib_assert(log.isValid(), "This function will infinite loop");
InputInfoPipe input(_t("Type a sentence"), log);
bool clear_input = false;//We can choose whether or not to clear the input
do
{
input.addMessage("Add a sentence ending with a period");
ThreadFunctions::RequestSleep(TimeSpan(5.0));
}
while(!input.getinput(clear_input).endsWith("."));
clear_input = true;//alternatively use input.clearInput();
log.addMessage("The sentence: "+input.getinput(clear_input));
}
It is a good idea to inherit from InputInfoPipe by providing some sort of signal common signal interaction. This can be used in conjunction with certain dialogs to take in options.
This will be discussed further later on.
Now that we understand how functions can use the logs and pipelines, how do we turn that information into something useful?
Log* log = new Log();
AsciiFileOutputStream stream("info.txt");
log->addStream(stream);
doSomething(log);
delete log;//stream will be automatically removed.
Steams can listen to any number of logs, and logs can have any number of steams.
Log* log_a = new Log();
Log* log_b = new Log();
AsciiFileOutputStream* ascii = new stream("info.txt");
STDOutputStream* std_out = new STDOutputStream();//logs to std::cout
log_a->addStream(ascii);
log_a->addStream(std_out);
log_b->addStream(ascii);
log_b->addStream(std_out);
doSomething(log_a);
delete log_a;//streams will be automatically removed.
doSomething(log_b);//this will also write to the stream
delete ascii;//automatically removed from Logs
doSomething(log_b);
In addition, this makes it easy to add custom behavior to any steam by inheriting from InfoStream and implementing the functions you wish to handle.