-
-
Notifications
You must be signed in to change notification settings - Fork 311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Delay finding and associating a build file with a (handle) uri #1647
Delay finding and associating a build file with a (handle) uri #1647
Conversation
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## master #1647 +/- ##
==========================================
- Coverage 76.12% 75.89% -0.24%
==========================================
Files 34 34
Lines 10007 10022 +15
==========================================
- Hits 7618 7606 -12
- Misses 2389 2416 +27 ☔ View full report in Codecov by Sentry. |
3b2a85c
to
6130513
Compare
src/DocumentStore.zig
Outdated
@@ -484,7 +484,9 @@ runtime_zig_version: *const ?ZigVersionWrapper, | |||
lock: std.Thread.RwLock = .{}, | |||
handles: std.StringArrayHashMapUnmanaged(*Handle) = .{}, | |||
build_files: std.StringArrayHashMapUnmanaged(*BuildFile) = .{}, | |||
build_files_lock: std.Thread.RwLock = .{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is a new RwLock
for build files needed?
src/Server.zig
Outdated
load_build_configuration: DocumentStore.Uri, | ||
run_build_on_save, | ||
|
||
fn deinit(self: Job, allocator: std.mem.Allocator) void { | ||
switch (self) { | ||
.incoming_message => |parsed_message| parsed_message.deinit(), | ||
.generate_diagnostics => |uri| allocator.free(uri), | ||
.find_build_file_for_uri => |uri| allocator.free(uri), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these switch cases here can be combined.
src/DocumentStore.zig
Outdated
if (std.process.can_spawn and !self.bare_instance_no_server) { | ||
const Server = @import("Server.zig"); | ||
const server = @fieldParentPtr(Server, "document_store", self); | ||
|
||
server.job_queue_lock.lock(); | ||
defer server.job_queue_lock.unlock(); | ||
|
||
try server.job_queue.ensureUnusedCapacity(1); | ||
server.job_queue.writeItemAssumeCapacity(.{ | ||
.find_build_file_for_uri = try server.allocator.dupe(u8, uri), | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we can avoid this dependency on Server
and the new bare_instance_no_server
by making the DocumentStore
take in a ?*const std.Thread.Pool
at init time.
src/DocumentStore.zig
Outdated
} | ||
|
||
pub fn findBuildFileForUri(self: *DocumentStore, uri: Uri) error{OutOfMemory}!void { | ||
const handle = self.getHandle(uri) orelse return; | ||
if (isBuildFile(handle.uri) and !isInStd(handle.uri)) { | ||
_ = self.getOrLoadBuildFile(handle.uri); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unrelated to finding the associated_build_file
so why does this happen here?
src/DocumentStore.zig
Outdated
if (std.process.can_spawn and !self.bare_instance_no_server) { | ||
const Server = @import("Server.zig"); | ||
const server = @fieldParentPtr(Server, "document_store", self); | ||
|
||
server.job_queue_lock.lock(); | ||
defer server.job_queue_lock.unlock(); | ||
|
||
try server.job_queue.ensureUnusedCapacity(1); | ||
server.job_queue.writeItemAssumeCapacity(.{ | ||
.find_build_file_for_uri = try server.allocator.dupe(u8, uri), | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have also delayed the collection imports and cimports which can cause wrong results until findBuildFileForUri
is done.
The code that prints the Opened document {s} with build file {s}
message should also be removed because there is no guarantee that at that point in time, the associated build file has been found.
src/DocumentStore.zig
Outdated
return handle; | ||
} | ||
|
||
pub fn findBuildFileForUri(self: *DocumentStore, uri: Uri) error{OutOfMemory}!void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function potentially writes to associated_build_file
, import_uris
and cimports
of Handle
while other threads may read from them -> Data Race
I just realized that you had converted your PR into a Draft, sorry for doing the review too early. |
It's actually appreciated — thank you. I'm scrapping (the approach on) this one and could use ideas on how to solve it. Haven't tested but I thought about refactoring out the find build file logic into a fn (similar to the one in this PR) and if there are no matches here Lines 1420 to 1424 in b328500
call it again? Con: extra file system operations or Keep a |
Having one function that does the entire "search for build file" job would be a great improvement. This is what I'm thinking of to get the issue resolved: This can be viewed a separate issue but its kind of related: |
be99b0f
to
f081c70
Compare
f081c70
to
7d27e60
Compare
src/DocumentStore.zig
Outdated
@@ -1063,7 +1101,7 @@ fn uriInImports( | |||
}; | |||
gop.key_ptr.* = handle.uri; | |||
|
|||
if (handle.associated_build_file) |associated_build_file_uri| { | |||
if (handle.associated_build_file orelse try self.doubleCheckPotentialBuildFileUri(handle)) |associated_build_file_uri| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reading from handle.associated_build_file
should be protected with a lock because other threads may write into it by calling doubleCheckPotentialBuildFileUri
on the same handle.
I would suggest to replace handle.associated_build_file orelse try self.doubleCheckPotentialBuildFileUri(handle)
with something like try handle.getAssociatedBuildFile(store)
and hide the associated_build_file
field by moving it into the impl
struct of Handle
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Within that fn, is a return self.associated_build_file;
guaranteed to make a copy or should the lock be held outside?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Within the function, you are holding a lock so the read from self.associated_build_file
is protected but outside the function, you are not holding a lock which makes accessing self.associated_build_file
a potential data race.
In short, always hold the lock of the handle when accessing self.associated_build_file
.
Does that make it more clear what the problem is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get the data race, my question is if the handle.getAssociatedBuildFile(store)
is
// lock defer unlock here
return self.associated_build_file;
is it guaranteed to make a copy which can be safely read outside the fn or should it be similar toBuildFile.tryLockConfig
src/DocumentStore.zig
Outdated
switch (handle.potential_build_file_uris.items.len) { | ||
0 => {}, | ||
else => |count| blk: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This nesting can be removed.
src/DocumentStore.zig
Outdated
pub fn hasConfig(self: *BuildFile) bool { | ||
self.impl.mutex.lock(); | ||
defer self.impl.mutex.unlock(); | ||
return if (self.impl.config) |_| true else false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return if (self.impl.config) |_| true else false; | |
return self.impl.config != null; |
src/DocumentStore.zig
Outdated
handle.associated_build_file = handle.potential_build_file_uris.items[index]; | ||
break :ok true; | ||
} | ||
} else unreachable; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the else unreachable
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The for
is guaranteed at least one entry/iteration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is the number of iterations related to whether or the not the else path is reached?
for (handle.potential_build_file_uris.items[1..], 1..) |build_file_uri, index| {
const build_file = self.getOrLoadBuildFile(build_file_uri) orelse break :ok false;
if (!build_file.hasConfig()) break :ok false;
if (try self.uriAssociatedWithBuild(build_file, handle.uri)) {
handle.associated_build_file = handle.potential_build_file_uris.items[index];
break :ok true;
}
// if this code path here is reached on the last iteration the you hit unreachable.
// I don't see why it shouldn't be possible to reach on the last iteration.
// The handle may not be associated with any of the potential build files.
} else unreachable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL; I thought it'd be only if the condition failed.
src/DocumentStore.zig
Outdated
_ = self.getOrLoadBuildFile(handle.potential_build_file_uris.items[0]) orelse break :blk; | ||
handle.associated_build_file = handle.potential_build_file_uris.items[0]; | ||
if (count == 1) break :blk; | ||
const ok: bool = ok: for (handle.potential_build_file_uris.items[1..], 1..) |build_file_uri, index| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about have a loop with the following structure?
handle.associated_build_file = for (handle.potential_build_file_uris.items) { ... };
You can have a special check before for handle.potential_build_file_uris.items.len == 1
. This should avoid having the boolean to reset the associated_build_file after the loop.
src/DocumentStore.zig
Outdated
@@ -588,6 +600,32 @@ fn getOrLoadBuildFile(self: *DocumentStore, uri: Uri) ?*BuildFile { | |||
return gop.value_ptr.*; | |||
} | |||
|
|||
pub fn doubleCheckPotentialBuildFileUri(self: *DocumentStore, handle: *Handle) error{OutOfMemory}!?Uri { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have a handle with multiple potential build files but none of them are associated to the handle, then this functions returns null
. Every subsequent call will then do its entire work again and then still return null
. There should be some kind of caching. This should also help with reducing contention on the handle mutex.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say the issue is when there are none, otherwise if they all've been ran the first one sticks.
I'm thinking maybe passing an optional handle uri to getOrLoadBuildFile to do the association after the file has been ran (for every build file other than the first one). It'd be easier to pass a *Handle, but having an uri and calling getHandle on it ensures the handle is still valid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it possible to return null when searching through multiple potential build files because of the if (!ok) handle.associated_build_file = null;
line? If !ok
then we return null
and any subsequent call would also return null
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Equipped with the knowledge of how for() {} else
works I think correcting the else to else true;
should handle this condition;
7d27e60
to
738d0e5
Compare
Did the 'pass an optional handle uri to getOrLoadBuildFile to do the association after the file has been ran', but it had a blind spot where if you had a 'build.zig' file open it would be created without the "callback", would get loaded, but have no config when checked via the "callback", so a recheck in Should we keep the "callback" complexity or have the "on demand" only logic? |
657fcda
to
b2740b2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we keep the "callback" complexity or have the "on demand" only logic?
I would consider your current "callback" approach to be acceptable (but a bit complex). I you want to do the "on demand" approach then this implementation of getAssociatedBuildFileUri
could be feasible.
Keep in mind that I have not tested this code and that TODO should definitely be resolved.
pub fn getAssociatedBuildFileUri(self: *Handle, store: *DocumentStore) ?Uri {
self.impl.lock.lock();
defer self.impl.lock.unlock();
switch(associated_build_file) {
.none => return null,
.unresolved => |potential_build_file_uris| {
// should there be special logic if `potential_build_file_uris.len == 1`?
var has_missing_build_config = false;
for (potential_build_file_uris) |build_file_uri| {
const build_file = self.getOrLoadBuildFile(build_file_uri, handle.uri) orelse continue; // continue or break?
if (!build_file.hasConfig()) {
has_missing_build_config = true;
continue;
}
if (!try self.uriAssociatedWithBuild(build_file, handle.uri)) {
// TODO
// the given `build_file_uri` should be removed from `potential_build_file_uris`
// so that future calls don't redo all this work on every call.
continue;
}
/// the associated build file has been resolved so there is no need to store the potential build files.
handle.potential_build_file_uris.clearAndFree(handle.allocator);
associateBuildFileUri = .{ .resolved = build_file.uri };
return build_file.uri;
};
if (!has_missing_build_config) {
/// there is no associated build file so there is no need to store the potential build files.
handle.potential_build_file_uris.clearAndFree(handle.allocator);
associated_build_file = .none;
}
// when build configs are missing we keep the state at .unresolved so that
// future calls will retry until all build config are resolved.
// Then will have a conclusive result on whether or not there is a associated build file.
return null;
},
.resolved => |uri| return uri,
}
}
associated_build_file: union(enum) {
/// The Handle has no associated build file (build.zig).
none,
/// The associated build file (build.zig) has not been resolved yet.
/// Stores a list of potential build files.
unresolved: []const Uri,
/// The associated build file (build.zig) has been successfully resolved.
resolved: Uri,
} = .unresolved,
src/DocumentStore.zig
Outdated
if (handle.associated_build_file) |associated_build_file_uri| { | ||
return std.mem.eql(u8, associated_build_file_uri, build_file_uri); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been added in #863 to make the build.zig
discovery faster.
This PR should be checked against https://github.com/marlersoft/zigwin32.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noice - a clear objective! ..also, I put those back in 😅, which restored same timings as main
.
..but it was still slow (on my machine/fs), ..so I took it further 🤪
/// holds a `shared` lock on success | ||
/// Release with `handle.unlockShared();` | ||
pub fn getAssociatedBuildFileUri(self: *Handle, store: *DocumentStore, associate_if_pending: bool) ?Uri { | ||
if (!self.impl.lock.tryLockShared()) return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why tryLockShared
and not lockShared
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of the calls are for completions, so I didn't consider it necessary to succeed.
I will evaluate all the locks again, but I keep picturing a scenario where
I enter a .
, don't get completions, bkspc, enter .
x3 and have 3 threads blocked
/// holds a 'shared' lock on success | ||
/// release with `build_file.unlockShared();` | ||
pub fn getConfig(self: *BuildFile) ?BuildConfig { | ||
if (!self.impl.lock.tryLockShared()) return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why tryLockShared
and not lockShared
?
src/DocumentStore.zig
Outdated
|
||
handle.impl.associated_build_file = blk: for (handle.potential_build_file_uris.items, 0..) |build_file_uri, index| { | ||
const build_file = self.getOrLoadBuildFile(build_file_uri, handle.uri) orelse break :blk .pending; | ||
if (!build_file.hasConfig()) break :blk .pending; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it continue if there is a missing config?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But then it would reach the else => .none
, unless I come around and implement your suggestion
dc40e12
to
e5ede4b
Compare
e5ede4b
to
340ac48
Compare
Current logic requires the modules available in a build.zig file to be known in order to correctly associate the file with an uri.
See #1629
Take N2