try to index media immediately on import

Since indexing is supplemental, it can be made a background task that can occur
at more or less any time. So we run it as soon as possible.
This commit is contained in:
itsmattkc
2020-01-17 03:58:29 +11:00
parent 63b3c60017
commit 653709dc84
28 changed files with 410 additions and 735 deletions
+2 -2
View File
@@ -14,13 +14,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
add_subdirectory(index)
set(OLIVE_SOURCES
${OLIVE_SOURCES}
task/task.h
task/task.cpp
task/taskmanager.h
task/taskmanager.cpp
task/taskthread.h
task/taskthread.cpp
PARENT_SCOPE
)
+22
View File
@@ -0,0 +1,22 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2019 Olive Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
task/index/index.h
task/index/index.cpp
PARENT_SCOPE
)
+26
View File
@@ -0,0 +1,26 @@
#include "index.h"
#include "codec/decoder.h"
IndexTask::IndexTask(StreamPtr stream) :
stream_(stream)
{
SetTitle(tr("Indexing %1:%2").arg(stream_->footage()->filename(), QString::number(stream_->index())));
}
void IndexTask::Action()
{
if (stream_->footage()->decoder().isEmpty()) {
emit Failed(QStringLiteral("Stream has no decoder"));
} else {
DecoderPtr decoder = Decoder::CreateFromID(stream_->footage()->decoder());
decoder->set_stream(stream_);
decoder->Open();
decoder->Index();
decoder->Close();
emit Succeeeded();
}
}
+20
View File
@@ -0,0 +1,20 @@
#ifndef INDEXTASK_H
#define INDEXTASK_H
#include "project/item/footage/footage.h"
#include "task/task.h"
class IndexTask : public Task
{
public:
IndexTask(StreamPtr stream);
protected:
virtual void Action() override;
private:
StreamPtr stream_;
};
#endif // INDEXTASK_H
+10 -139
View File
@@ -21,168 +21,39 @@
#include "task.h"
Task::Task() :
status_(kWaiting),
thread_(this),
text_(tr("Task")),
title_(tr("Task")),
cancelled_(false)
{
connect(&thread_, SIGNAL(finished()), this, SLOT(ThreadComplete()));
}
bool Task::Start()
void Task::Start()
{
// Tasks can only start if they're waiting
if (status_ != kWaiting) {
return false;
}
Action();
// Check if this task has any dependencies (tasks that should complete before this one starts)
for (int i=0;i<dependencies_.size();i++) {
Task* dependency = dependencies_.at(i);
if (dependency->status() == kWaiting || dependency->status() == kWorking) {
// We need this task to finish before this task can start, so keep waiting
return false;
} else if (dependency->status() == kError) {
// A dependency errored, so this task is likely invalid too
set_error(tr("A dependency task failed"));
set_status(kError);
return false;
}
}
cancelled_ = false;
// Run Prologue() function
if (!Prologue()) {
set_status(kError);
return false;
}
set_status(kWorking);
thread_.start();
return true;
emit Finished();
}
bool Task::Prologue()
const QString &Task::GetTitle()
{
return true;
}
bool Task::Action()
{
return true;
}
bool Task::Epilogue()
{
return true;
}
const Task::Status &Task::status()
{
return status_;
}
const QString &Task::text()
{
return text_;
}
const QString &Task::error()
{
return error_;
}
void Task::AddDependency(Task *dependency)
{
// Dependencies cannot be added if the Task is working or complete
Q_ASSERT(status_ == kWaiting);
dependencies_.append(dependency);
}
void Task::ResetState()
{
if (status_ == kWaiting) {
return;
}
if (status_ == kWorking) {
Cancel();
}
cancelled_ = false;
set_error(QString());
set_status(kWaiting);
return title_;
}
void Task::Cancel()
{
if (status_ == kWaiting) {
set_status(kFinished);
}
if (status_ != kWorking) {
return;
}
cancelled_ = true;
// FIXME: Should we limit the wait time?
thread_.wait();
}
void Task::set_error(const QString &s)
void Task::SetErrorText(const QString &s)
{
error_ = s;
}
void Task::set_text(const QString &s)
void Task::SetTitle(const QString &s)
{
text_ = s;
title_ = s;
}
bool Task::cancelled()
bool Task::IsCancelled()
{
return cancelled_;
}
void Task::set_status(const Task::Status &status)
{
status_ = status;
if (status_ == kFinished || status_ == kError) {
emit Finished();
}
emit StatusChanged(status_);
}
void Task::ThreadComplete()
{
// thread_.result() will be set to the return value of Action()
bool succeeded = thread_.result();
// Run the Prologue() function for any final tasks
// User cancelling is not considered an error, so we need to check it too
if (succeeded && !cancelled()) {
succeeded = Epilogue();
}
// If everything succeeded, we set the status accordingly
if (succeeded) {
set_status(kFinished);
} else {
set_status(kError);
}
}
+22 -142
View File
@@ -24,8 +24,6 @@
#include <memory>
#include <QObject>
#include "task/taskthread.h"
/**
* @brief A base class for background tasks running in Olive.
*
@@ -47,31 +45,17 @@ class Task : public QObject
{
Q_OBJECT
public:
/**
* @brief The Status enum
*
* All states that a Task can be in. When subclassing, you don't need to set the Task's status as the base class
* does that automatically.
*/
enum Status {
/// This Task is yet to start
kWaiting,
/// This Task is currently running (see Action())
kWorking,
/// This Task has completed successfully
kFinished,
/// This Task failed and could not complete
kError
};
/**
* @brief Task Constructor
*/
Task();
/**
* @brief Retrieve the current title of this Task
*/
const QString& GetTitle();
public slots:
/**
* @brief Try to start this Task
*
@@ -81,29 +65,18 @@ public:
* This function also checks its dependency Tasks and will only start if all of them are complete. If they are still
* working, this function will return FALSE and the status will continue to be kWaiting. If any of them failed, this
* Task will also fail - this function will return FALSE and the status will be set to kError.
*
* @return
*
* TRUE if the Task started, FALSE if not.
*/
bool Start();
void Start();
/**
* @brief Perform opening tasks before main Task thread begins
* @brief Cancel the Task
*
* If a Task needs to perform any actions in the main thread before starting the Task's thread, (e.g. copying or
* altering information) this function should be overridden and those actions should be performed here. It's
* guaranteed that Prologue() will run in the main thread, and as such, functions here should remain as minimal as
* possible as to not block the main thread for a noticeable amount of time. If your Task does not need any such
* actions, you don't need to override this.
*
* @return
*
* TRUE if the prologue was successful and we can start the Task now. If Prologue returns FALSE, the thread is never
* created and Action()/Epilogue() are never run.
* Sends a signal to the Task to stop as soon as possible. Always call this directly or connect with
* Qt::DirectConnection, or else it'll be queued *after* the task has already finished.
*/
virtual bool Prologue();
void Cancel();
protected:
/**
* @brief The main Task function which is run in a separate thread
*
@@ -119,86 +92,15 @@ public:
* could not finish, not if the Task found a negative result (see Task documentation for details). Before returning
* FALSE, it's recommended to use set_error() to signal to the user what caused the failure.
*/
virtual bool Action();
virtual void Action() = 0;
/**
* @brief Perform any closing Tasks in the main thread after the Task thread finishes
*
* It's likely your Task modifies data used throughout the program in some way, and to prevent race conditions, it's
* recommended to work with "copies" of that data in Action() (which is run in separate thread) and never
* access/modify any data used in other threads. Then, after the Action() thread is complete, that data can be used to
* "apply" that data in the main thread here.
*
* As this runs in the main thread, these functions shouldn't be kept fairly minimal to prevent blocking the main
* thread.
*
* @return
*
* TRUE if the Epilogue completed successfully. FALSE if not. A FALSE result here is considered a complete failure
* of the Task, even though the bulk of the processing has been performed in Action().
*/
virtual bool Epilogue();
/**
* @brief Current status of the Task
*
* @return
*
* A member of the Task::Status enum.
*/
const Status& status();
/**
* @brief Retrieve the current title of this Task
*/
const QString& text();
/**
* @brief Retrieve the current error message (empty if no error)
*/
const QString& error();
/**
* @brief Add a dependency Task
*
* If another Task needs to complete before this one can begin, it can be added as a "dependency task". If a task
* has dependencies, Start() will not start the task until the dependency tasks have all completed. If any of the
* dependency tasks fail, this Task will also fail before starting.
*
* Dependencies can only be added if the Task is kWaiting.
*
* Naturally Tasks should never be dependent on each other. Circular dependencies will result in Tasks that never
* begin.
*
* @param dependency
*/
void AddDependency(Task* dependency);
/**
* @brief Reset this Task back to the waiting state
*/
void ResetState();
public slots:
/**
* @brief Cancel the Task
*
* Sends a signal to the Task to stop and waits for the Task to finish before returning. Tasks must be responsive to
* cancelling so that the main thread doesn't halt for too long.
*
* Cancel()'s function is fairly simple, it sets cancelled_ to TRUE and waits for the thread to return. It's the
* responsibility of the code in Action() to be able to respond quickly to cancelled_ changing.
*/
void Cancel();
protected:
/**
* @brief Set the error message
*
* It is recommended to use this if your Action() function ever returns FALSE to tell the user why the failure
* occurred.
*/
void set_error(const QString& s);
void SetErrorText(const QString& s);
/**
* @brief Set the Task title
@@ -207,19 +109,14 @@ protected:
* and shouldn't need to change during the life of the Task. To show an error message, it's recommended to use
* set_error() instead.
*/
void set_text(const QString& s);
void SetTitle(const QString& s);
/**
* @brief Returns whether the thread has been explicitly cancelled or not
*/
bool cancelled();
bool IsCancelled();
signals:
/**
* @brief Signal emitted whenever the Task status changes
*/
void StatusChanged(Task::Status s);
/**
* @brief Signal emitted whenever progress is made
*
@@ -231,9 +128,10 @@ signals:
*/
void ProgressChanged(int p);
/**
* @brief Signal emitted when the Task finishes whether it succeeded or failed
*/
void Succeeeded();
void Failed(const QString& error);
void Finished();
/**
@@ -242,30 +140,12 @@ signals:
void Removed();
private:
/**
* @brief Set the status of this Task (also emits StatusChanged())
*/
void set_status(const Task::Status& status);
Status status_;
TaskThread thread_;
QString text_;
QString title_;
QString error_;
QList<Task*> dependencies_;
QAtomicInt cancelled_;
bool cancelled_;
private slots:
/**
* @brief A slot when the inner thread completes either successfully or unsuccessfully
*/
void ThreadComplete();
};
using TaskPtr = std::shared_ptr<Task>;
#endif // TASK_H
+166 -66
View File
@@ -23,119 +23,219 @@
#include <QDebug>
#include <QThread>
TaskManager TaskManager::instance_;
TaskManager* TaskManager::instance_ = nullptr;
TaskManager::TaskManager()
TaskManager::TaskManager() :
active_thread_count_(0)
{
maximum_task_count_ = QThread::idealThreadCount();
// Initialize threads to run tasks on
threads_.resize(QThread::idealThreadCount());
for (int i=0;i<threads_.size();i++) {
QThread* t = new QThread(this);
t->start(QThread::LowPriority);
threads_.replace(i, {t, false});
}
}
TaskManager::~TaskManager()
{
Clear();
// First send the signal to all tasks to start cancelling
foreach (const TaskContainer& task_info, tasks_) {
if (task_info.status == kWorking) {
task_info.task->Cancel();
}
}
// Next, signal each thread to quit as its next event in the queue
foreach (const ThreadContainer& tc, threads_) {
tc.thread->quit();
}
// Wait for each thread's event queue to finish
foreach (const ThreadContainer& tc, threads_) {
tc.thread->wait();
// This is technically unnecessary since each QThread is a child of this object, but we may as well
delete tc.thread;
}
// Finally delete all task objects (they shouldn't have been deleted by TaskSucceeded() or TaskFailed() because our
// event queue shouldn't be active by this point
foreach (const TaskContainer& task_info, tasks_) {
delete task_info.task;
}
}
void TaskManager::CreateInstance()
{
instance_ = new TaskManager();
}
void TaskManager::DestroyInstance()
{
delete instance_;
}
TaskManager *TaskManager::instance()
{
return &instance_;
return instance_;
}
void TaskManager::AddTask(TaskPtr t)
void TaskManager::AddTask(Task* t)
{
// Connect Task's status signal to the Callback
connect(t.get(), SIGNAL(StatusChanged(Task::Status)), this, SLOT(TaskCallback(Task::Status)));
connect(t, &Task::Succeeeded, this, &TaskManager::TaskSucceeded, Qt::QueuedConnection);
connect(t, &Task::Failed, this, &TaskManager::TaskFailed, Qt::QueuedConnection);
// Add the Task to the queue
tasks_.append(t);
tasks_.append({t, kWaiting});
// Emit signal that a Task was added
emit TaskAdded(t.get());
emit TaskAdded(t);
// Scan through queue and start any Tasks that can (including this one)
StartNextWaiting();
}
void TaskManager::Clear()
{
// Delete Tasks from memory
for (int i=0;i<tasks_.size();i++) {
tasks_.at(i)->Cancel();
}
tasks_.clear();
}
void TaskManager::StartNextWaiting()
{
// Count the tasks that are currently active
int working_count = 0;
// If there are no tasks in the queue, there is nothing to be done
if (tasks_.isEmpty()) {
return;
}
for (int i=0;i<tasks_.size();i++) {
TaskPtr t = tasks_.at(i);
if (t->status() == Task::kWorking) {
// Task is active, add it to the count
working_count++;
} else if (t->status() == Task::kWaiting) {
// Task is waiting and we have available threads, try to start it
if (t->Start()) {
// If it started, add it to the working count
working_count++;
}
// If all threads are occupied, nothing to be done
if (active_thread_count_ == threads_.size()) {
return;
}
// Create a list of tasks that are waiting
QList<Task*> waiting_tasks;
foreach (const TaskContainer& task_info, tasks_) {
if (task_info.status == kWaiting) {
waiting_tasks.append(task_info.task);
}
}
// Check if the count exceeds our maximum threads, if so stop here
if (working_count == maximum_task_count_) {
break;
// No tasks waiting to start
if (waiting_tasks.isEmpty()) {
return;
}
// For any inactive threads,
for (int i=0;i<threads_.size();i++) {
if (!threads_.at(i).active) {
// This thread is inactive and needs a new Task
Task* task = waiting_tasks.takeFirst();
task->moveToThread(threads_.at(i).thread);
threads_[i].active = true;
active_thread_count_++;
SetTaskStatus(task, kWorking);
QMetaObject::invokeMethod(task,
"Start",
Qt::QueuedConnection);
if (active_thread_count_ == threads_.size() || waiting_tasks.isEmpty()) {
break;
}
}
}
}
void TaskManager::DeleteTask(Task *t)
{
// Cancel the task
t->Cancel();
if (GetTaskStatus(t) == kWorking) {
// Send a signal to the task to cancel, it will likely continue to cancel in the background after it's removed
t->Cancel();
}
// Remove instances of Task from queue
for (int i=0;i<tasks_.size();i++) {
if (tasks_.at(i).get() == t) {
emit t->Removed();
if (tasks_.at(i).task == t) {
tasks_.removeAt(i);
break;
}
}
emit t->Removed();
if (GetTaskStatus(t) != kWorking) {
// If the task isn't doing anything, we can simply delete it
delete t;
}
}
void TaskManager::TaskFinished(Task* task)
{
// Set this thread's active value to false
for (int i=0;i<threads_.size();i++) {
if (threads_.at(i).thread == task->thread()) {
threads_[i].active = false;
}
}
// Decrement the active thread count
active_thread_count_--;
}
TaskManager::TaskStatus TaskManager::GetTaskStatus(Task *t)
{
foreach (const TaskContainer& container, tasks_) {
if (container.task == t) {
return container.status;
}
}
return kError;
}
void TaskManager::SetTaskStatus(Task *t, TaskStatus status)
{
for (int i=0;i<tasks_.size();i++) {
TaskContainer& cont = tasks_[i];
if (cont.task == t) {
cont.status = status;
break;
}
}
}
void TaskManager::TaskCallback(Task::Status status)
void TaskManager::TaskSucceeded()
{
if (status == Task::kFinished || status == Task::kError) {
// The Task has finished, we can start a new one
StartNextWaiting();
Task* task_sender = static_cast<Task*>(sender());
if (status == Task::kFinished) {
// The Task was successful, remove this Task from the queue
DeleteTask(static_cast<Task*>(sender()));
SetTaskStatus(task_sender, kFinished);
TaskFinished(task_sender);
// Delete this task
DeleteTask(task_sender);
}
void TaskManager::TaskFailed()
{
Task* task_sender = static_cast<Task*>(sender());
SetTaskStatus(task_sender, kError);
TaskFinished(task_sender);
// If this task has already been deleted, we'll free its memory now
bool was_deleted = true;
for (int i=0;i<tasks_.size();i++) {
if (tasks_.at(i).task == task_sender) {
was_deleted = false;
break;
}
}
}
TaskManager::AddTaskCommand::AddTaskCommand(TaskPtr t, QUndoCommand *parent) :
QUndoCommand(parent),
task_(t)
{
}
void TaskManager::AddTaskCommand::redo()
{
TaskManager::instance()->AddTask(task_);
}
void TaskManager::AddTaskCommand::undo()
{
TaskManager::instance()->DeleteTask(task_.get());
task_->ResetState();
if (was_deleted) {
delete task_sender;
}
}
+53 -66
View File
@@ -24,6 +24,7 @@
#include <QVector>
#include <QUndoCommand>
#include "common/constructors.h"
#include "task/task.h"
/**
@@ -50,25 +51,11 @@ public:
*/
virtual ~TaskManager();
/**
* @brief Deleted copy constructor
*/
TaskManager(const TaskManager& other) = delete;
DISABLE_COPY_MOVE(TaskManager)
/**
* @brief Deleted move constructor
*/
TaskManager(TaskManager&& other) = delete;
static void CreateInstance();
/**
* @brief Deleted copy assignment
*/
TaskManager& operator=(const TaskManager& other) = delete;
/**
* @brief Deleted move assignment
*/
TaskManager& operator=(TaskManager&& other) = delete;
static void DestroyInstance();
static TaskManager* instance();
@@ -87,38 +74,7 @@ public:
*
* The task to add and run. TaskManager takes ownership of this Task and will be responsible for freeing it.
*/
void AddTask(TaskPtr t);
/**
* @brief Forcibly cancel all commands and clear them
*/
void Clear();
/**
* @brief Undoable command for adding a Task to the TaskManager
*/
class AddTaskCommand : public QUndoCommand {
public:
AddTaskCommand(TaskPtr t, QUndoCommand* parent = nullptr);
/**
* @brief Adds the Task to the TaskManager
*
* If there are available threads, TaskManager will start running it.
*/
virtual void redo() override;
/**
* @brief Undoes adding the Task
*
* If the Task is running, it is cancelled. Then the Task is removed from the TaskManager and the Task's state is
* reset.
*/
virtual void undo() override;
private:
TaskPtr task_;
};
void AddTask(Task *t);
signals:
/**
@@ -131,6 +87,36 @@ signals:
void TaskAdded(Task* t);
private:
/**
* @brief The Status enum
*
* All states that a Task can be in. When subclassing, you don't need to set the Task's status as the base class
* does that automatically.
*/
enum TaskStatus {
/// This Task is yet to start
kWaiting,
/// This Task is currently running (see Action())
kWorking,
/// This Task has completed successfully
kFinished,
/// This Task failed and could not complete
kError
};
struct TaskContainer {
Task* task;
TaskStatus status;
};
struct ThreadContainer {
QThread* thread;
bool active;
};
/**
* @brief Scan through the task queue and start any Tasks that are able to start
*
@@ -155,35 +141,36 @@ private:
*/
void DeleteTask(Task* t);
void TaskFinished(Task *task);
TaskStatus GetTaskStatus(Task* t);
void SetTaskStatus(Task* t, TaskStatus status);
/**
* @brief Internal task array
*/
QVector<TaskPtr> tasks_;
QVector<TaskContainer> tasks_;
/**
* @brief Constant set at run-time of how many Tasks can run concurrently
*
* Currently set in the Constructor to QThread::idealThreadCount()
* @brief Background threads to run tasks on
*/
int maximum_task_count_;
QVector<ThreadContainer> threads_;
/**
* @brief Value for how many threads are currently active
*/
int active_thread_count_;
/**
* @brief TaskManager singleton instance
*/
static TaskManager instance_;
static TaskManager* instance_;
private slots:
/**
* @brief Callback when a Task's status changes
*
* When a Task is added, it's connected to this so TaskManager is alerted whenever a Task's status changes. It can
* therefore keep track of Tasks starting, completing, or failing.
*
* @param status
*
* The new status of the Task
*/
void TaskCallback(Task::Status status);
void TaskSucceeded();
void TaskFailed();
};
-39
View File
@@ -1,39 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "taskthread.h"
#include "task/task.h"
TaskThread::TaskThread(Task *parent) :
parent_(parent),
result_(false)
{
}
void TaskThread::run()
{
result_ = parent_->Action();
}
bool TaskThread::result()
{
return result_;
}
-50
View File
@@ -1,50 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef TASKTHREAD_H
#define TASKTHREAD_H
#include <QThread>
class Task;
/**
* @brief An internal class only used by Task.
*
* TaskThread is a simple QThread subclass designed to create a thread and run the
* Task's Action() function. It also stores the result of Action() which can be read using result() when the thread
* signals that it has finished().
*/
class TaskThread : public QThread
{
public:
TaskThread(Task* parent);
virtual void run() override;
bool result();
private:
Task* parent_;
bool result_;
};
#endif // TASKTHREAD_H