1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-11-23 17:30:17 +01:00

AUP3: Simplify checkpoing and add checkpoint progress dialog

Also move the project file closure out of the ProjectFileIO
destructor and does it before the project file window is
destroyed.
This commit is contained in:
Leland Lucius
2020-07-20 11:10:31 -05:00
parent d2b4a0e488
commit 84f363ee07
6 changed files with 171 additions and 233 deletions

View File

@@ -14,6 +14,7 @@ Paul Licameli split from AudacityProject.cpp
#include <sqlite3.h>
#include <wx/crt.h>
#include <wx/frame.h>
#include <wx/progdlg.h>
#include <wx/sstream.h>
#include <wx/xml/xml.h>
@@ -134,16 +135,10 @@ static const char *SafeConfig =
// Configuration to provide "Fast" connections
static const char *FastConfig =
"PRAGMA <schema>.locking_mode = EXCLUSIVE;"
"PRAGMA <schema>.locking_mode = SHARED;"
"PRAGMA <schema>.synchronous = OFF;"
"PRAGMA <schema>.journal_mode = OFF;";
// Configuration to provide "Fast" connections
static const char *FastWalConfig =
"PRAGMA <schema>.locking_mode = EXCLUSIVE;"
"PRAGMA <schema>.synchronous = OFF;"
"PRAGMA <schema>.journal_mode = WAL;";
// This singleton handles initialization/shutdown of the SQLite library.
// It is needed because our local SQLite is built with SQLITE_OMIT_AUTOINIT
// defined.
@@ -273,34 +268,12 @@ void ProjectFileIO::Init( AudacityProject &project )
mpProject = project.shared_from_this();
// Kick off the checkpoint thread
mCheckpointStop = false;
mCheckpointThread = std::thread([this]{ CheckpointThread(); });
}
ProjectFileIO::~ProjectFileIO()
{
if (mDB)
{
// Save the filename since CloseDB() will clear it
wxString filename = mFileName;
// Not much we can do if this fails. The user will simply get
// the recovery dialog upon next restart.
if (CloseDB())
{
// At this point, we are shutting down cleanly and if the project file is
// still in the temp directory it means that the user has chosen not to
// save it. So, delete it.
if (mTemporary)
{
wxFileName temp(FileNames::TempDir());
if (temp == wxPathOnly(filename))
{
wxRemoveFile(filename);
}
}
}
}
wxASSERT_MSG(mDB == nullptr, wxT("Project file was not closed at shutdown"));
// Tell the checkpoint thread to shutdown
{
@@ -313,20 +286,19 @@ ProjectFileIO::~ProjectFileIO()
mCheckpointThread.join();
}
#define USE_DIFFERENT_DB_HANDLE 1
void ProjectFileIO::CheckpointThread()
{
mCheckpointStop = false;
while (true)
{
sqlite3 *db;
{
// Wait for something to show up in the queue or for the stop signal
// Wait for work or the stop signal
std::unique_lock<std::mutex> lock(mCheckpointMutex);
mCheckpointCondition.wait(lock,
[&]
{
return mCheckpointStop || !mCheckpointWork.empty();
return mCheckpointWaitingPages || mCheckpointStop;
});
// Requested to stop, so bail
@@ -335,45 +307,33 @@ void ProjectFileIO::CheckpointThread()
break;
}
// Pop the head of the queue
db = mCheckpointWork.front();
mCheckpointWork.pop_front();
// Capture the number of pages that need checkpointing and reset
mCheckpointCurrentPages = mCheckpointWaitingPages;
mCheckpointWaitingPages = 0;
#if defined(USE_DIFFERENT_DB_HANDLE)
// Lock out other while the checkpoint is running
// Lock out others while the checkpoint is running
mCheckpointActive.lock();
#endif
}
// Open another connection to the DB to prevent blocking the main thread.
sqlite3 *db = nullptr;
if (sqlite3_open(mFileName, &db) == SQLITE_OK)
{
// Get it's filename
const char *filename = sqlite3_db_filename(db, nullptr);
// Configure it to be safe
Config(db, SafeConfig);
// And kick off the checkpoint. This may not checkpoint ALL frames
// in the WAL. They'll be gotten the next time around.
sqlite3_wal_checkpoint_v2(db, nullptr, SQLITE_CHECKPOINT_PASSIVE, nullptr, nullptr);
#if defined(USE_DIFFERENT_DB_HANDLE)
// Open it. We don't use the queued database pointer because that
// would block the main thread if it tried to use it at the same time.
db = nullptr;
if (sqlite3_open(filename, &db) == SQLITE_OK)
#endif
{
#if defined(USE_DIFFERENT_DB_HANDLE)
// Configure it to be safe
Config(db, SafeConfig);
#endif
// And do the actual checkpoint. This may not checkpoint ALL frames
// in the WAL. They'll be gotten the next time round.
sqlite3_wal_checkpoint_v2(db, nullptr, SQLITE_CHECKPOINT_PASSIVE, nullptr, nullptr);
}
#if defined(USE_DIFFERENT_DB_HANDLE)
// All done...okay to call with null pointer if open failed.
// All done.
sqlite3_close(db);
// Reset
mCheckpointCurrentPages = 0;
// Checkpoint is complete
mCheckpointActive.unlock();
#endif
}
}
@@ -387,7 +347,7 @@ int ProjectFileIO::CheckpointHook(void *data, sqlite3 *db, const char *schema, i
// Qeuue the database pointer for our checkpoint thread to process
std::lock_guard<std::mutex> guard(that->mCheckpointMutex);
that->mCheckpointWork.push_back(db);
that->mCheckpointWaitingPages = pages;
that->mCheckpointCondition.notify_one();
return SQLITE_OK;
@@ -548,26 +508,35 @@ bool ProjectFileIO::CloseDB()
if (mDB)
{
// Uninstall our checkpoint hook
// Uninstall our checkpoint hook so that no additional checkpoints
// are sent our way. (Though this shouldn't really happen.)
sqlite3_wal_hook(mDB, nullptr, nullptr);
// Remove this connection from the checkpoint work queue, if any
// Display a progress dialog if there's active or pending checkpoints
if (mCheckpointWaitingPages || mCheckpointCurrentPages)
{
std::lock_guard<std::mutex> guard(mCheckpointMutex);
TranslatableString title = XO("Checkpointing project");
mCheckpointWork.erase(std::remove(mCheckpointWork.begin(),
mCheckpointWork.end(),
mDB),
mCheckpointWork.end());
// Get access to the active tracklist
auto pProject = mpProject.lock();
if (pProject)
{
title = XO("Checkpointing %s").Format(pProject->GetProjectName());
}
wxGenericProgressDialog pd(title.Translation(),
XO("This may take several seconds").Translation(),
300000, // range
nullptr, // parent
wxPD_APP_MODAL | wxPD_ELAPSED_TIME | wxPD_SMOOTH);
while (mCheckpointWaitingPages || mCheckpointCurrentPages)
{
wxMilliSleep(50);
pd.Pulse();
}
}
#if defined(USE_DIFFERENT_DB_HANDLE)
// Wait for ANY active checkpoint to complete as it may be for
// this database
mCheckpointActive.lock();
mCheckpointActive.unlock();
#endif
// Close the DB
rc = sqlite3_close(mDB);
if (rc != SQLITE_OK)
@@ -856,7 +825,7 @@ bool ProjectFileIO::InstallSchema(sqlite3 *db, const char *schema /* = "main" */
sql.Printf(ProjectFileSchema, ProjectFileID, ProjectFileVersion);
sql.Replace("<schema>", schema);
rc = sqlite3_exec(db, sql.mb_str().data(), nullptr, nullptr, nullptr);
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
@@ -998,7 +967,7 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
wxString sql;
sql.Printf("ATTACH DATABASE '%s' AS outbound;", destpath);
rc = sqlite3_exec(db, sql.mb_str().data(), nullptr, nullptr, nullptr);
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
@@ -1133,17 +1102,11 @@ sqlite3 *ProjectFileIO::CopyTo(const FilePath &destpath,
}
// Ensure attached DB connection gets configured
Config(destdb, FastWalConfig);
Config(destdb, SafeConfig);
// Tell cleanup everything is good to go
success = true;
// IMPORTANT: At this point, the DB handle is setup with WAL journaling,
// but synchronous is OFF. Only use this connection if you can tolerate
// a crash/power failure. Otherwise, reconfigure it using SafeConfig.
//
// It's left configured this way so that the caller can do additional
// work using a faster, but VOLATILE connection.
return destdb;
}
@@ -1225,9 +1188,19 @@ void ProjectFileIO::Vacuum(const std::shared_ptr<TrackList> &tracks)
// close time will still occur
mHadUnused = true;
// Don't vacuum if this is a temporary project or if Go figure out if we should at least try
// Don't vacuum if this is a temporary project or if it's deteremined there not
// enough unused blocks to make it worthwhile
if (IsTemporary() || !ShouldVacuum(tracks))
{
// Delete the AutoSave doc it if exists
if (IsModified())
{
// PRL: not clear what to do if the following fails, but the worst should
// be, the project may reopen in its present state as a recovery file, not
// at the last saved state.
(void) AutoSaveDelete();
}
return;
}
@@ -1746,7 +1719,7 @@ bool ProjectFileIO::ImportProject(const FilePath &fileName)
wxString sql;
sql.Printf("ATTACH DATABASE 'file:%s?immutable=1&mode=ro' AS inbound;", fileName);
rc = sqlite3_exec(db, sql.mb_str().data(), nullptr, nullptr, nullptr);
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
{
SetDBError(
@@ -1933,7 +1906,7 @@ bool ProjectFileIO::ImportProject(const FilePath &fileName)
wxString sql;
sql.Printf("DELETE FROM main.sampleblocks WHERE blockid = %lld", blockid);
rc = sqlite3_exec(db, sql.mb_str().data(), nullptr, nullptr, nullptr);
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
if (rc != SQLITE_OK)
{
// This is non-fatal...it'll just get cleaned up the next
@@ -2167,7 +2140,6 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
{
wxRemoveFile(origName);
}
}
else
{
@@ -2209,6 +2181,9 @@ bool ProjectFileIO::SaveProject(const FilePath &fileName)
// And make it the active project file
UseConnection(newDB, fileName);
// Install our checkpoint hook
sqlite3_wal_hook(mDB, CheckpointHook, this);
}
ProjectSerializer doc;
@@ -2252,21 +2227,6 @@ bool ProjectFileIO::SaveCopy(const FilePath& fileName)
return false;
}
bool success = false;
auto cleanup = finally([&]
{
if (db)
{
(void) sqlite3_close(db);
}
if (!success)
{
wxRemoveFile(fileName);
}
});
ProjectSerializer doc;
WriteXMLHeader(doc);
WriteXML(doc);
@@ -2274,11 +2234,40 @@ bool ProjectFileIO::SaveCopy(const FilePath& fileName)
// Write the project doc to the new DB
if (!WriteDoc("project", doc, db))
{
wxRemoveFile(fileName);
return false;
}
// Tell the finally block to behave
success = true;
// All good...close the database
(void) sqlite3_close(db);
return true;
}
bool ProjectFileIO::CloseProject()
{
if (mDB)
{
// Save the filename since CloseDB() will clear it
wxString filename = mFileName;
// Not much we can do if this fails. The user will simply get
// the recovery dialog upon next restart.
if (CloseDB())
{
// If this is a temporary project, we no longer want to keep the
// project file.
if (mTemporary)
{
// This is just a safety check.
wxFileName temp(FileNames::TempDir());
if (temp == wxPathOnly(filename))
{
wxRemoveFile(filename);
}
}
}
}
return true;
}
@@ -2350,12 +2339,37 @@ void ProjectFileIO::SetDBError(const TranslatableString &msg)
wxLogDebug(wxT(" Lib error: %s"), mLibraryError.Debug());
printf(" Lib error: %s", mLibraryError.Debug().mb_str().data());
}
abort();
wxASSERT(false);
}
void ProjectFileIO::Bypass(bool bypass)
void ProjectFileIO::SetBypass()
{
mBypass = bypass;
// Determine if we can bypass sample block deletes during shutdown.
//
// IMPORTANT:
// If the project was vacuumed, then we MUST bypass further
// deletions since the new file doesn't have the blocks that the
// Sequences expect to be there.
mBypass = true;
// Only permanent project files need cleaning at shutdown
if (!IsTemporary() && !WasVacuumed())
{
// If we still have unused blocks, then we must not bypass deletions
// during shutdown. Otherwise, we would have orphaned blocks the next time
// the project is opened.
//
// An example of when dead blocks will exist is when a user opens a permanent
// project, adds a track (with samples) to it, and chooses not to save the
// changes.
if (HadUnused())
{
mBypass = false;
}
}
return;
}
bool ProjectFileIO::ShouldBypass()