Skip to content

Commit

Permalink
projects: introduce file metadata cache
Browse files Browse the repository at this point in the history
We can use this to avoid rebuilding targets if the output files
they depend on are unchanged after a rebuild. This speeds up large
projects like Geary where many targets depend on a "core" target whose
VAPI remains unchanged if the user is only modifing method bodies.
  • Loading branch information
Prince781 committed Apr 22, 2022
1 parent 753cad0 commit f38b3b2
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 19 deletions.
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ vls_src = files([
'projects/ccproject.vala',
'projects/compilation.vala',
'projects/defaultproject.vala',
'projects/filecache.vala',
'projects/mesonproject.vala',
'projects/project.vala',
'projects/textdocument.vala',
Expand Down
8 changes: 7 additions & 1 deletion src/projects/buildtask.vala
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ class Vls.BuildTask : BuildTarget {
*/
public ArrayList<File> candidate_inputs { get; private set; default = new ArrayList<File> (); }

public BuildTask (string build_dir, string output_dir, string name, string id, int no,
private FileCache _file_cache;

public BuildTask (FileCache file_cache, string build_dir, string output_dir, string name, string id, int no,
string[] compiler, string[] args, string[] sources, string[] generated_sources,
string[] target_output_files,
string language) throws Error {
base (output_dir, name, id, no);
_file_cache = file_cache;
// don't pipe stderr since we want to print that if something goes wrong
this.build_dir = build_dir;
launcher = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE);
Expand Down Expand Up @@ -300,6 +303,9 @@ class Vls.BuildTask : BuildTarget {
outfile_ostream.splice (process_output_istream, OutputStreamSpliceFlags.NONE, cancellable);
}
}
// update the file metadata cache
foreach (var file in output)
_file_cache.update (file, cancellable);
last_updated = new DateTime.now ();
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/projects/ccproject.vala
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ class Vls.CcProject : Project {
}

if (cc.command[0].contains ("valac"))
build_targets.add (new Compilation (cc.directory, cc.file ?? @"CC#$i", @"CC#$i", i,
build_targets.add (new Compilation (file_cache, cc.directory, cc.file ?? @"CC#$i", @"CC#$i", i,
cc.command[0:1], cc.command[1:cc.command.length],
new string[]{}, new string[]{}, new string[]{}));
else
build_targets.add (new BuildTask (cc.directory, cc.directory, cc.file ?? @"CC#$i", @"CC#$i", i,
build_targets.add (new BuildTask (file_cache, cc.directory, cc.directory, cc.file ?? @"CC#$i", @"CC#$i", i,
cc.command[0:1], cc.command[1:cc.command.length],
new string[]{}, new string[]{},
new string[]{}, "unknown"));
Expand All @@ -83,8 +83,8 @@ class Vls.CcProject : Project {
return true;
}

public CcProject (string root_path, string cc_location, Cancellable? cancellable = null) throws Error {
base (root_path);
public CcProject (string root_path, string cc_location, FileCache file_cache, Cancellable? cancellable = null) throws Error {
base (root_path, file_cache);

var root_dir = File.new_for_path (root_path);
var cc_json_file = File.new_for_commandline_arg_and_cwd (cc_location, root_path);
Expand Down
28 changes: 25 additions & 3 deletions src/projects/compilation.vala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ class Vls.Compilation : BuildTarget {
private HashSet<string> _metadata_dirs = new HashSet<string> ();
private HashSet<string> _gresources_dirs = new HashSet<string> ();

/**
* This helps us determine which files have remained the same after an
* update.
*/
private FileCache _file_cache;

/**
* These are files that are part of the project.
*/
Expand Down Expand Up @@ -98,11 +104,12 @@ class Vls.Compilation : BuildTarget {
*/
public HashMap<string, Vala.Symbol> cname_to_sym { get; private set; default = new HashMap<string, Vala.Symbol> (); }

public Compilation (string output_dir, string name, string id, int no,
public Compilation (FileCache file_cache, string output_dir, string name, string id, int no,
string[] compiler, string[] args, string[] sources, string[] generated_sources,
string?[] target_output_files,
string[]? sources_content = null) throws Error {
base (output_dir, name, id, no);
_file_cache = file_cache;
directory = output_dir;

// parse arguments
Expand Down Expand Up @@ -401,10 +408,15 @@ class Vls.Compilation : BuildTarget {
configure (cancellable);

bool stale = false;
foreach (BuildTarget dep in dependencies.values) {
if (dep.last_updated.compare (last_updated) > 0) {
bool updated_file = false;

foreach (Map.Entry<File, BuildTarget> dep in dependencies) {
if (_file_cache[dep.key].last_updated.compare (last_updated) > 0) {
stale = true;
break;
} else if (dep.value.last_updated.compare (last_updated) > 0) {
// dep was updated but file is the same
updated_file = true;
}
}
foreach (TextDocument doc in _project_sources.values) {
Expand All @@ -418,7 +430,17 @@ class Vls.Compilation : BuildTarget {
cancellable.set_error_if_cancelled ();
// TODO: cancellable compilation
compile ();
} else if (updated_file) {
// even if the files are unchanged after updates, we need to
// silently update the last_updated property of this target at the
// very least, in order to maintain the invariant that a build
// target is always "last updated" after its dependencies
last_updated = new DateTime.now ();
}

// update all output files
foreach (var file in output)
_file_cache.update (file, cancellable);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/projects/defaultproject.vala
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ using Gee;
* A project without any backend. Mainly useful for editing one file.
*/
class Vls.DefaultProject : Project {
public DefaultProject (string root_path) {
base (root_path);
public DefaultProject (string root_path, FileCache file_cache) {
base (root_path, file_cache);
}

public override bool reconfigure_if_stale (Cancellable? cancellable = null) throws Error {
Expand Down Expand Up @@ -53,7 +53,7 @@ class Vls.DefaultProject : Project {
warning ("failed to parse interpreter line");
}
}
btarget = new Compilation (root_path, uri, uri, build_targets.size,
btarget = new Compilation (file_cache, root_path, uri, uri, build_targets.size,
{"valac"}, args, sources, {}, {}, content != null ? new string[]{content} : null);
// build it now so that information is available immediately on
// file open (other projects compile on LSP initialize(), so they don't
Expand Down
146 changes: 146 additions & 0 deletions src/projects/filecache.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/* filecache.vala
*
* Copyright 2022 Princeton Ferro <princetonferro@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

using Gee;

/**
* In-memory file metadata cache. Used to check whether files remain the same
* after context updates.
*/
class Vls.FileCache : Object {
/**
* File metadata.
*/
public class ContentStatus {
/**
* The time this information was last updated. This may be earlier than
* the time the file was last updated if the file is the same after an
* update.
*/
public DateTime last_updated { get; set; }

/**
* The time this file was last updated. Can be null if we're unable to
* query this info from the file system.
*/
public DateTime? file_last_updated { get; set; }

/**
* The size of the file.
*/
public size_t size { get; set; }

/**
* The checksum of the file.
*/
public string checksum { get; set; }

/**
* Create a new content status.
*
* @param data data loaded from a file
* @param last_modified the last time the file was modified
*/
public ContentStatus (Bytes data, DateTime? last_modified) {
this.last_updated = new DateTime.now ();
this.file_last_updated = last_modified;
this.size = data.get_size ();
// MD5 is fastest and we don't have any security issues even if there are collisions
this.checksum = Checksum.compute_for_bytes (ChecksumType.MD5, data);
}

/**
* Create a new content status for an empty/non-existent file.
*/
public ContentStatus.empty () {
this.last_updated = new DateTime.now ();
this.file_last_updated = null;
this.size = 0;
this.checksum = Checksum.compute_for_data (ChecksumType.MD5, {});
}
}

private HashMap<File, ContentStatus> _content_cache;

public FileCache () {
_content_cache = new HashMap<File, ContentStatus> (Util.file_hash, Util.file_equal);
}

/**
* Updates the file in the cache. Will perform I/O, but will check the file
* modification time first. If the file does not exist, its metadata will
* be created in an empty configuration.
*
* @param file the file to add or update in the cache
* @param cancellable (optional) a way to cancel the I/O operation
*/
public void update (File file, Cancellable? cancellable = null) throws Error {
ContentStatus? status = _content_cache[file];
DateTime? last_modified = null;
bool file_exists = false;
try {
FileInfo info = file.query_info (FileAttribute.TIME_MODIFIED, FileQueryInfoFlags.NONE, cancellable);
#if GLIB_2_62
last_modified = info.get_modification_date_time ();
#else
TimeVal time_last_modified = info.get_modification_time ();
last_modified = new DateTime.from_iso8601 (time_last_modified.to_iso8601 (), null);
#endif
file_exists = true;
} catch (IOError.NOT_FOUND e) {
// we only want to catch file-not-found errors. if there was some other error
// with querying the file system, we want to exit this function
}

if (file_exists && last_modified == null)
warning ("could not get last modified time of %s", file.get_uri ());

if (status == null) {
// the file is being entered into the cache for the first time
if (file_exists)
_content_cache[file] = new ContentStatus (file.load_bytes (cancellable), last_modified);
else
_content_cache[file] = new ContentStatus.empty ();
return;
}

// the file is in the cache already.
// check modification time to avoid having to recompute the hash
if (last_modified != null && status.file_last_updated != null && last_modified.compare (status.file_last_updated) <= 0)
return;

// recompute the hash
ContentStatus new_status;
if (file_exists)
new_status = new ContentStatus (file.load_bytes (cancellable), last_modified);
else
new_status = new ContentStatus.empty ();
if (new_status.checksum == status.checksum && new_status.size == status.size)
return;

_content_cache[file] = new_status;
return;
}

/**
* Gets the content status of the file if it's in the cache.
*/
public new ContentStatus? get (File file) {
return _content_cache[file];
}
}
10 changes: 6 additions & 4 deletions src/projects/mesonproject.vala
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,8 @@ class Vls.MesonProject : Project {

// finally, construct the build target
if (first_source.language == "vala")
build_targets.add (new Compilation (target_private_output_dir,
build_targets.add (new Compilation (file_cache,
target_private_output_dir,
meson_target_info.name,
meson_target_info.id,
elem_idx,
Expand Down Expand Up @@ -533,7 +534,8 @@ class Vls.MesonProject : Project {
executes_generated_program = true;
}

var added_task = new BuildTask (build_dir,
var added_task = new BuildTask (file_cache,
build_dir,
target_private_output_dir,
meson_target_info.name,
meson_target_info.id,
Expand Down Expand Up @@ -750,8 +752,8 @@ class Vls.MesonProject : Project {
base.build_if_stale (cancellable);
}

public MesonProject (string root_path, Cancellable? cancellable = null) throws Error {
base (root_path);
public MesonProject (string root_path, FileCache file_cache, Cancellable? cancellable = null) throws Error {
base (root_path, file_cache);
this.build_dir = DirUtils.make_tmp (@"vls-meson-$(str_hash (root_path))-XXXXXX");
reconfigure_if_stale (cancellable);
}
Expand Down
9 changes: 8 additions & 1 deletion src/projects/project.vala
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,15 @@ abstract class Vls.Project : Object {
*/
private HashMap<File, FileMonitor> monitored_files = new HashMap<File, FileMonitor> (Util.file_hash, Util.file_equal);

protected Project (string root_path) {
/**
* Use this in projects to keep track of target outputs and avoid
* rebuilding dependent targets.
*/
protected FileCache file_cache;

protected Project (string root_path, FileCache file_cache) {
this.root_path = root_path;
this.file_cache = file_cache;
}

/**
Expand Down
12 changes: 9 additions & 3 deletions src/server.vala
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ class Vls.Server : Jsonrpc.Server {
*/
HashSet<string> open_files = new HashSet<string> ();

/**
* Use this in projects to keep track of target outputs and avoid
* rebuilding dependent targets.
*/
FileCache file_cache = new FileCache ();

static construct {
Process.@signal (ProcessSignal.INT, () => {
Server.received_signal = true;
Expand Down Expand Up @@ -375,7 +381,7 @@ class Vls.Server : Jsonrpc.Server {
// TODO: autotools, make(?), cmake(?)
if (meson_file.query_exists (cancellable)) {
try {
backend_project = new MesonProject (root_path, cancellable);
backend_project = new MesonProject (root_path, file_cache, cancellable);
} catch (Error e) {
if (!(e is ProjectError.VERSION_UNSUPPORTED)) {
show_message (client, @"Failed to initialize Meson project - $(e.message)", MessageType.Error);
Expand All @@ -388,7 +394,7 @@ class Vls.Server : Jsonrpc.Server {
foreach (var cc_file in cc_files) {
string cc_file_path = Util.realpath (cc_file.get_path ());
try {
backend_project = new CcProject (root_path, cc_file_path, cancellable);
backend_project = new CcProject (root_path, cc_file_path, file_cache, cancellable);
debug ("[initialize] initialized CcProject with %s", cc_file_path);
break;
} catch (Error e) {
Expand All @@ -412,7 +418,7 @@ class Vls.Server : Jsonrpc.Server {
}

// always have default project
default_project = new DefaultProject (root_path);
default_project = new DefaultProject (root_path, file_cache);

// build and publish diagnostics
foreach (var project in new_projects) {
Expand Down

0 comments on commit f38b3b2

Please sign in to comment.