unspecified attributes on load are no longer defaulted - #2688
unspecified attributes on load are no longer defaulted#2688DanielLacina wants to merge 54 commits into
Conversation
…alues and new config flush on graph load
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark 'Rust Benchmark'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 2.
| Benchmark suite | Current: 3e65345 | Previous: 9823ef7 | Ratio |
|---|---|---|---|
lotr_graph/num_edges |
3 ns/iter (± 0) |
0 ns/iter (± 0) |
+∞ |
lotr_graph/num_nodes |
4 ns/iter (± 0) |
1 ns/iter (± 0) |
4 |
lotr_graph/graph_latest |
3 ns/iter (± 0) |
0 ns/iter (± 0) |
+∞ |
lotr_graph_materialise/materialize |
7634180 ns/iter (± 41997) |
1564816 ns/iter (± 35303) |
4.88 |
lotr_graph_window_100/num_nodes |
11 ns/iter (± 0) |
5 ns/iter (± 0) |
2.20 |
lotr_graph_window_100_materialise/materialize |
7620110 ns/iter (± 107762) |
1669150 ns/iter (± 10700) |
4.57 |
lotr_graph_window_10_materialise/materialize |
2987981 ns/iter (± 10377) |
971980 ns/iter (± 4278) |
3.07 |
lotr_graph_subgraph_10pc/num_nodes |
17 ns/iter (± 0) |
4 ns/iter (± 0) |
4.25 |
lotr_graph_subgraph_10pc_materialise/materialize |
1996701 ns/iter (± 23596) |
334634 ns/iter (± 1287) |
5.97 |
lotr_graph_subgraph_10pc_windowed_materialise/materialize |
1175941 ns/iter (± 17638) |
230399 ns/iter (± 2617) |
5.10 |
lotr_graph_window_50_layered/has_node_existing |
361 ns/iter (± 19) |
129 ns/iter (± 12) |
2.80 |
lotr_graph_window_50_layered_materialise/materialize |
25670356 ns/iter (± 160777) |
3488825 ns/iter (± 24948) |
7.36 |
lotr_graph_persistent_window_50_layered/num_edges_temporal |
516766 ns/iter (± 5048) |
192686 ns/iter (± 1569) |
2.68 |
lotr_graph_persistent_window_50_layered/has_node_existing |
386 ns/iter (± 320) |
174 ns/iter (± 83) |
2.22 |
lotr_graph_persistent_window_50_layered_materialise/materialize |
43449211 ns/iter (± 187043) |
5298035 ns/iter (± 147912) |
8.20 |
This comment was automatically generated by workflow using github-action-benchmark.
There was a problem hiding this comment.
Most methods in raphtory-graphql should just take Config, since the server will parse the args and convert them into a Config on startup. The server exclusively uses MaterializedGraph which is not exposed in the python API, so the constructors for MaterializedGraph should also just take a Config.
In core raphtory, the user-facing constructor methods in Graph and PersistentGraph are the only ones that need to take a ConfigArgs.
TLDR; ConfigArgs are only needed at the end-user boundary which are the constructor methods exposed through python and the ServerArgs on raphtory-graphql.
| @@ -521,33 +522,36 @@ impl ValidWriteableGraphFolder { | |||
| pub fn write_graph_data( | |||
There was a problem hiding this comment.
Unrelated to changes in this PR, but can you add a newline above to separate methods?
|
|
||
| fn new(config: Self::Config, graph_dir: Option<&Path>) -> Result<Self, StorageError>; | ||
|
|
||
| fn load(graph_dir: &Path) -> Result<Self, StorageError>; |
There was a problem hiding this comment.
Rather than associate type ConfigArgs with PersistenceStrategy, I think it makes sense to move it to ConfigOps.
| } | ||
|
|
||
| pub trait ConfigOps: Serialize + DeserializeOwned + Args + Sized { | ||
| type NewConfigArgs: ConfigArgsOps; |
There was a problem hiding this comment.
To follow up on #2688 (comment), you would change this to type ConfigArgs and use () for BaseConfig and WriteAndMergeConfigArgs for WriteAndMergeConfig.
| pub fn load_with_config(path: impl AsRef<Path>, config: Config) -> Result<Self, GraphError> { | ||
| pub fn load_with_config( | ||
| path: impl AsRef<Path>, | ||
| config_args: ConfigArgs, |
There was a problem hiding this comment.
I'd like Storage to take an already built Config (we seem to be doing that with new anyways).
In graph.rs, have ConfigArgs get converted into a Config and pass it down to the relevant storage. This is the right boundary for the conversion to happen.
You'll have to change PersistenceStrategy::load_with_config to also take in a Config. I recommend creating a ConfigArgs::load_from_path method that internally calls update and combines the passed-in args + args on disk. The caller can then call into_config and pass the generate Config into Storage.
| ) -> Result<MaterializedGraph, ClientError> { | ||
| let encoded = self.receive_graph(path).await?; | ||
| url_decode_graph(encoded, Config::default()).map_err(ClientError::from) | ||
| url_decode_graph(encoded, ConfigArgs::default()).map_err(ClientError::from) |
There was a problem hiding this comment.
This should take a Config.
| let folder_clone = folder.clone(); | ||
| let g: MaterializedGraph = blocking_compute(move || { | ||
| url_decode_graph_at(graph, folder_clone.graph_folder(), config) | ||
| url_decode_graph_at(graph, folder_clone.graph_folder(), config.into_args()) |
There was a problem hiding this comment.
This should be a Config.
| let server = block_on(GraphServer::new( | ||
| work_dir, | ||
| app_config, | ||
| ConfigArgs::default(), |
There was a problem hiding this comment.
This should be a Config and ServerArgs.graph_config should be a ConfigArgs.
| blocking_compute(move || { | ||
| let (is_dirty, new_graph) = writeable_folder.write_graph_data(graph, config)?; | ||
| let (is_dirty, new_graph) = | ||
| writeable_folder.write_graph_data(graph, config.into_args())?; |
There was a problem hiding this comment.
This should just take Config.
| &cache, | ||
| create_index, | ||
| config, | ||
| config_args, |
There was a problem hiding this comment.
This should just take Config.
| fn max_node_page_len(&self) -> Option<u32>; | ||
|
|
||
| fn max_edge_page_len(&self) -> Option<u32>; | ||
|
|
||
| fn node_types(&self) -> &[String]; | ||
|
|
||
| fn with_max_node_page_len(self, page_len: u32) -> Self; | ||
|
|
||
| fn with_max_edge_page_len(self, page_len: u32) -> Self; | ||
|
|
||
| fn with_node_types(&self, node_types: impl IntoIterator<Item = impl AsRef<str>>) -> Self; |
There was a problem hiding this comment.
I don't think you need these methods, all you need in this trait is into_config and load_from_path.
There was a problem hiding this comment.
I think we could simplify it even further, all this needs is the update method to apply the config args to an existing config (where that may be loaded from disk or default) with signature (I would probably call this apply instead of update as well) fn apply(&self, config: &mut Self::Config)
There was a problem hiding this comment.
with that, we can have a default implementation for into_config which is simply
fn into_config(self) -> Self::Config {
let mut config = Self::Config::default();
self.apply(&mut config);
config
}| } | ||
|
|
||
| fn update(&mut self, new: Self); | ||
| fn update(&mut self, new_args: Self::ConfigArgs); |
There was a problem hiding this comment.
We should move this update method into ConfigArgs.
| max_edge_page_len: Option<u32>, | ||
| } | ||
|
|
||
| impl ConfigArgsOps for BaseConfigArgs { |
There was a problem hiding this comment.
I think you can remove this, we only need to implement ConfigArgsOps on WriteAndMergeConfigArgs.
| } | ||
| } | ||
|
|
||
| impl ConfigArgsOps for () { |
There was a problem hiding this comment.
I think either this or the BaseConfigArgs will be needed if load_with_config ends up existing in the OS raphtory apis
| } | ||
|
|
||
| pub trait ConfigOps: Serialize + DeserializeOwned + Args + Sized { | ||
| type ConfigArgs: ConfigArgsOps; |
There was a problem hiding this comment.
If we move load_from_dir and save_to_dir to ConfigArgs, it can be responsible for the full lifecycle of loading/saving config from disk.
There was a problem hiding this comment.
I would keep those here (they should always return/save a full config, not a ConfigArgs), see my comment on ConfigArgs, it would only have a way to update an existing, config, nothing else.
There was a problem hiding this comment.
once that is the case, this probably doesn't actually need the associated type at all
| fn update(&mut self, new: Self); | ||
| fn update(&mut self, new_args: Self::ConfigArgs); | ||
|
|
||
| fn into_args(self) -> Self::ConfigArgs; |
There was a problem hiding this comment.
I think most places that call into_args should just pass a Config anyways?
There was a problem hiding this comment.
We could then get rid of into_args.
There was a problem hiding this comment.
agreed, this shouldn't be needed
There was a problem hiding this comment.
The main current use for this is in the graphql server, maybe the better answer for that would be for the server to store config args instead of config?
| pub fn load_with_config( | ||
| path: &(impl GraphPaths + ?Sized), | ||
| config: Config, | ||
| config_args: ConfigArgs, |
There was a problem hiding this comment.
This should be a Config but you'll run into problems below with Graph::load_with_config and PersistentGraph::load_with_config since those take ConfigArgs.
The way around this is to create a Storage object which takes Config and then convert that into a Graph or PersistentGraph.
There was a problem hiding this comment.
I disagree on this one, this is an end-user facing function and should take ConfigArgs to update only the desired fields of the original graph on disk. This is used in rust when loading a graph where you want to preserve the type of the graph (Event or Persistent).
Co-authored-by: Fadhil Abubaker <fadhil.abubaker@pometry.com>
ljeub-pometry
left a comment
There was a problem hiding this comment.
A bit more cleanup required
| pub fn load_with_config( | ||
| path: &(impl GraphPaths + ?Sized), | ||
| config: Config, | ||
| config_args: ConfigArgs, |
There was a problem hiding this comment.
I disagree on this one, this is an end-user facing function and should take ConfigArgs to update only the desired fields of the original graph on disk. This is used in rust when loading a graph where you want to preserve the type of the graph (Event or Persistent).
| fn max_node_page_len(&self) -> Option<u32>; | ||
|
|
||
| fn max_edge_page_len(&self) -> Option<u32>; | ||
|
|
||
| fn node_types(&self) -> &[String]; | ||
|
|
||
| fn with_max_node_page_len(self, page_len: u32) -> Self; | ||
|
|
||
| fn with_max_edge_page_len(self, page_len: u32) -> Self; | ||
|
|
||
| fn with_node_types(&self, node_types: impl IntoIterator<Item = impl AsRef<str>>) -> Self; |
There was a problem hiding this comment.
I think we could simplify it even further, all this needs is the update method to apply the config args to an existing config (where that may be loaded from disk or default) with signature (I would probably call this apply instead of update as well) fn apply(&self, config: &mut Self::Config)
| } | ||
|
|
||
| pub trait ConfigOps: Serialize + DeserializeOwned + Args + Sized { | ||
| type ConfigArgs: ConfigArgsOps; |
There was a problem hiding this comment.
I would keep those here (they should always return/save a full config, not a ConfigArgs), see my comment on ConfigArgs, it would only have a way to update an existing, config, nothing else.
| fn update(&mut self, new: Self); | ||
| fn update(&mut self, new_args: Self::ConfigArgs); | ||
|
|
||
| fn into_args(self) -> Self::ConfigArgs; |
There was a problem hiding this comment.
agreed, this shouldn't be needed
| } | ||
|
|
||
| pub trait ConfigOps: Serialize + DeserializeOwned + Args + Sized { | ||
| type ConfigArgs: ConfigArgsOps; |
There was a problem hiding this comment.
once that is the case, this probably doesn't actually need the associated type at all
| } | ||
| } | ||
|
|
||
| impl ConfigArgsOps for () { |
There was a problem hiding this comment.
I think either this or the BaseConfigArgs will be needed if load_with_config ends up existing in the OS raphtory apis
| } else { | ||
| let new_graph = graph.materialize_at_with_config(self.graph_folder(), config)?; | ||
| let new_graph = | ||
| graph.materialize_at_with_config(self.graph_folder(), config.into_args())?; |
There was a problem hiding this comment.
This is an interesting one, one possibility is that the server should actually store a ConfigArgs so it can only override some of the fields and leave everything else as it was on the original graph. The other thing is that materialize_at_with_config should actually be able to override the page sizes as it is creating a new graph. If we want this to be as type safe as possible, we would need separate args for this case and the load case where the page sizes cannot be overridden. Alternatively, we could have separate apply methods for the two scenarios and just ignore/error at runtime for the page sizes when they cannot be changed.
| path: impl AsRef<Path>, | ||
| path_for_decoded_graph: Option<&Path>, | ||
| config: Config, | ||
| config_args: ConfigArgs, |
There was a problem hiding this comment.
This will mean that all the decode methods can just take a config.
| fn max_node_page_len(&self) -> Option<u32>; | ||
|
|
||
| fn max_edge_page_len(&self) -> Option<u32>; | ||
|
|
||
| fn node_types(&self) -> &[String]; | ||
|
|
||
| fn with_max_node_page_len(self, page_len: u32) -> Self; | ||
|
|
||
| fn with_max_edge_page_len(self, page_len: u32) -> Self; | ||
|
|
||
| fn with_node_types(&self, node_types: impl IntoIterator<Item = impl AsRef<str>>) -> Self; |
There was a problem hiding this comment.
with that, we can have a default implementation for into_config which is simply
fn into_config(self) -> Self::Config {
let mut config = Self::Config::default();
self.apply(&mut config);
config
}| fn update(&mut self, new: Self); | ||
| fn update(&mut self, new_args: Self::ConfigArgs); | ||
|
|
||
| fn into_args(self) -> Self::ConfigArgs; |
There was a problem hiding this comment.
The main current use for this is in the graphql server, maybe the better answer for that would be for the server to store config args instead of config?
We already have unit tests in rust in cli.rs and integration-tests in python in test_cli_parsing.py
What changes were proposed in this pull request?
Synced config to disk on Graph.load, replaced Config with ConfigArgs so program knows what attributes are specified
to help get rid of unspecified attributes on load are no longer defaulted undesirable behavior.
Also, added tests to test graph config.
Why are the changes needed?
Unspecified attributes on load are defaulted. It automatically overrides previous config attributes if they're not specified which is undesirable. Config should be synced to disk after load.
Does this PR introduce any user-facing change? If yes is this documented?
Yes. Changing Config with ConfigArgs causes API change.
How was this patch tested?
Tested Graph.load and Graph behavior with config specified and used assertions to verify the config is in the right state.
Are there any further changes required?
Maybe