-
Notifications
You must be signed in to change notification settings - Fork 557
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
JSON Module rewrite with custom JSONPath implementation #974
base: main
Are you sure you want to change the base?
Changes from 5 commits
2463f3a
71617ee
c3b0ad1
7b534a6
7650f5f
6c9c347
50633a0
e2eaa0a
dce580f
005e98d
0265708
ebb4f25
46d27b3
84c0aca
02b6077
a32bd7e
919e2b4
bc6483d
a12baba
23d4ca3
9f437c3
4207c8d
243b136
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
using BenchmarkDotNet.Attributes; | ||
using BenchmarkDotNet.Engines; | ||
using GarnetJSON.JSONPath; | ||
using System.Text.Json.Nodes; | ||
|
||
namespace BDN.benchmark.Json; | ||
|
||
[MemoryDiagnoser] | ||
public class JsonPathQuery | ||
{ | ||
private JsonNode _jsonNode; | ||
|
||
private readonly Consumer _consumer = new Consumer(); | ||
|
||
[Params( | ||
"$.store.book[0].title", | ||
"$.store.book[*].author", | ||
"$.store.book[?(@.price < 10)].title", | ||
"$.store.bicycle.color", | ||
"$.store.book[*]", // all books | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: indents are wonky, just replace with a single space There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
"$.store..price", // all prices using recursive descent | ||
"$..author", // all authors using recursive descent | ||
"$.store.book[?(@.price > 10 && @.price < 20)]", // filtered by price range | ||
"$.store.book[?(@.category == 'fiction')]", // filtered by category | ||
"$.store.book[-1:]", // last book | ||
"$.store.book[:2]", // first two books | ||
"$.store.book[?(@.author =~ /.*Waugh/)]", // regex match on author | ||
"$..book[0,1]", // union of array indices | ||
"$..*", // recursive descent all nodes | ||
"$..['bicycle','price']", // recursive descent specfic node with name match | ||
"$..[?(@.price < 10)]", // recursive descent specfic node with conditionally match | ||
"$.store.book[?(@.author && @.title)]", // existence check | ||
"$.store.*" // wildcard child | ||
)] | ||
public string JsonPath { get; set; } | ||
|
||
[GlobalSetup] | ||
public void Setup() | ||
{ | ||
var jsonString = """ | ||
{ | ||
"store": { | ||
"book": [ | ||
{ | ||
"category": "reference", | ||
"author": "Nigel Rees", | ||
"title": "Sayings of the Century", | ||
"price": 8.95 | ||
}, | ||
{ | ||
"category": "fiction", | ||
"author": "Evelyn Waugh", | ||
"title": "Sword of Honour", | ||
"price": 12.99 | ||
} | ||
], | ||
"bicycle": { | ||
"color": "red", | ||
"price": 19.95 | ||
} | ||
} | ||
} | ||
"""; | ||
|
||
_jsonNode = JsonNode.Parse(jsonString); | ||
} | ||
|
||
[Benchmark] | ||
public void SelectNodes() | ||
{ | ||
var result = _jsonNode.SelectNodes(JsonPath); | ||
result.Consume(_consumer); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
using System.Text; | ||
using BenchmarkDotNet.Attributes; | ||
using Embedded.server; | ||
|
||
namespace BDN.benchmark.Operations | ||
{ | ||
/// <summary> | ||
/// Benchmark for ModuleOperations | ||
/// </summary> | ||
[MemoryDiagnoser] | ||
public class JsonOperations : OperationsBase | ||
{ | ||
// Existing commands | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Existing & new with regards to what? These would all be existing commands once the PR is merged... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My bad, removed it |
||
static ReadOnlySpan<byte> JSONGETCMD => "*3\r\n$8\r\nJSON.GET\r\n$2\r\nk3\r\n$1\r\n$\r\n"u8; | ||
static ReadOnlySpan<byte> JSONSETCMD => "*4\r\n$8\r\nJSON.SET\r\n$2\r\nk3\r\n$4\r\n$.f2\r\n$1\r\n2\r\n"u8; | ||
|
||
// New commands for different JsonPath patterns | ||
static ReadOnlySpan<byte> JSONGET_DEEP => "*3\r\n$8\r\nJSON.GET\r\n$4\r\nbig1\r\n$12\r\n$.data[0].id\r\n"u8; | ||
static ReadOnlySpan<byte> JSONGET_ARRAY => "*3\r\n$8\r\nJSON.GET\r\n$4\r\nbig1\r\n$13\r\n$.data[*]\r\n"u8; | ||
static ReadOnlySpan<byte> JSONGET_ARRAY_ELEMENTS => "*3\r\n$8\r\nJSON.GET\r\n$4\r\nbig1\r\n$13\r\n$.data[*].id\r\n"u8; | ||
static ReadOnlySpan<byte> JSONGET_FILTER => "*3\r\n$8\r\nJSON.GET\r\n$4\r\nbig1\r\n$29\r\n$.data[?(@.active==true)]\r\n"u8; | ||
static ReadOnlySpan<byte> JSONGET_RECURSIVE => "*3\r\n$8\r\nJSON.GET\r\n$4\r\nbig1\r\n$4\r\n$..*\r\n"u8; | ||
|
||
Request jsonGetCmd; | ||
Request jsonSetCmd; | ||
Request jsonGetDeepCmd; | ||
Request jsonGetArrayCmd; | ||
Request jsonGetArrayElementsCmd; | ||
Request jsonGetFilterCmd; | ||
Request jsonGetRecursiveCmd; | ||
|
||
private static string GenerateLargeJson(int items) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Please place the private methods after the public ones There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
{ | ||
var data = new System.Text.StringBuilder(); | ||
data.Append("{\"data\":["); | ||
|
||
for (int i = 0; i < items; i++) | ||
{ | ||
if (i > 0) data.Append(','); | ||
data.Append($$""" | ||
{ | ||
"id": {{i}}, | ||
"name": "Item{{i}}", | ||
"active": {{(i % 2 == 0).ToString().ToLower()}}, | ||
"value": {{i * 100}}, | ||
"nested": { | ||
"level1": { | ||
"level2": { | ||
"value": {{i}} | ||
} | ||
} | ||
} | ||
} | ||
"""); | ||
} | ||
|
||
data.Append("]}"); | ||
return data.ToString(); | ||
} | ||
|
||
private void RegisterModules() | ||
{ | ||
server.Register.NewModule(new NoOpModule.NoOpModule(), [], out _); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the No-op module needed here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed |
||
server.Register.NewModule(new GarnetJSON.Module(), [], out _); | ||
} | ||
|
||
public override void GlobalSetup() | ||
{ | ||
base.GlobalSetup(); | ||
RegisterModules(); | ||
|
||
SetupOperation(ref jsonGetCmd, JSONGETCMD); | ||
SetupOperation(ref jsonSetCmd, JSONSETCMD); | ||
SetupOperation(ref jsonGetDeepCmd, JSONGET_DEEP); | ||
SetupOperation(ref jsonGetArrayCmd, JSONGET_ARRAY); | ||
SetupOperation(ref jsonGetArrayElementsCmd, JSONGET_ARRAY_ELEMENTS); | ||
SetupOperation(ref jsonGetFilterCmd, JSONGET_FILTER); | ||
SetupOperation(ref jsonGetRecursiveCmd, JSONGET_RECURSIVE); | ||
|
||
// Setup test data | ||
var largeJson = GenerateLargeJson(20); | ||
SlowConsumeMessage(Encoding.UTF8.GetBytes($"*4\r\n$8\r\nJSON.SET\r\n$4\r\nbig1\r\n$1\r\n$\r\n${largeJson.Length}\r\n{largeJson}\r\n")); | ||
|
||
// Existing setup | ||
SlowConsumeMessage("*4\r\n$8\r\nJSON.SET\r\n$2\r\nk3\r\n$1\r\n$\r\n$14\r\n{\"f1\":{\"a\":1}}\r\n"u8); | ||
SlowConsumeMessage(JSONGETCMD); | ||
SlowConsumeMessage(JSONSETCMD); | ||
} | ||
|
||
[Benchmark] | ||
public void ModuleJsonGetCommand() | ||
{ | ||
Send(jsonGetCmd); | ||
} | ||
|
||
[Benchmark] | ||
public void ModuleJsonSetCommand() | ||
{ | ||
Send(jsonSetCmd); | ||
} | ||
|
||
[Benchmark] | ||
public void ModuleJsonGetDeepPath() | ||
{ | ||
Send(jsonGetDeepCmd); | ||
} | ||
|
||
[Benchmark] | ||
public void ModuleJsonGetArrayPath() | ||
{ | ||
Send(jsonGetArrayCmd); | ||
} | ||
|
||
[Benchmark] | ||
public void ModuleJsonGetArrayElementsPath() | ||
{ | ||
Send(jsonGetArrayElementsCmd); | ||
} | ||
|
||
[Benchmark] | ||
public void ModuleJsonGetFilterPath() | ||
{ | ||
Send(jsonGetFilterCmd); | ||
} | ||
|
||
[Benchmark] | ||
public void ModuleJsonGetRecursive() | ||
{ | ||
Send(jsonGetRecursiveCmd); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -93,6 +93,15 @@ public static unsafe void WriteBulkString(ref (IMemoryOwner<byte>, int) output, | |
public static unsafe void WriteError(ref (IMemoryOwner<byte>, int) output, string errorMessage) | ||
{ | ||
var bytes = System.Text.Encoding.ASCII.GetBytes(errorMessage); | ||
WriteError(ref output, bytes); | ||
} | ||
|
||
/// <summary> | ||
/// Create output as error message, from given string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: update the comment There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
/// </summary> | ||
public static unsafe void WriteError(ref (IMemoryOwner<byte>, int) output, ReadOnlySpan<byte> errorMessage) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. General comment: this class should be internal, all calls to these methods should be through There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. General comment: while you're updating this file, for some reason there are 2 copyright comments on top, if you could remove the bottom one that'd be awesome :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
{ | ||
var bytes = errorMessage; | ||
// Get space for error | ||
var len = 1 + bytes.Length + 2; | ||
output.Item1 = MemoryPool.Rent(len); | ||
|
@@ -142,5 +151,18 @@ public static unsafe void WriteSimpleString(ref (IMemoryOwner<byte>, int) output | |
} | ||
output.Item2 = len; | ||
} | ||
|
||
public static unsafe void WriteDirect(ref (IMemoryOwner<byte>, int) output, ReadOnlySpan<byte> bytes) | ||
{ | ||
output.Item1 = MemoryPool.Rent(bytes.Length); | ||
fixed (byte* ptr = output.Item1.Memory.Span) | ||
{ | ||
var curr = ptr; | ||
// NOTE: Expected to always have enough space to write into pre-allocated buffer | ||
var success = RespWriteUtils.TryWriteDirect(bytes, ref curr, ptr + bytes.Length); | ||
Debug.Assert(success, "Insufficient space in pre-allocated buffer"); | ||
} | ||
output.Item2 = bytes.Length; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,13 @@ public abstract class CustomObjectFunctions | |
/// </summary> | ||
protected static unsafe void WriteError(ref (IMemoryOwner<byte>, int) output, string errorMessage) => CustomCommandUtils.WriteError(ref output, errorMessage); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
/// <summary> | ||
/// Writes the specified bytes directly to the output. | ||
/// </summary> | ||
/// <param name="output">The output buffer and its length.</param> | ||
/// <param name="bytes">The bytes to write.</param> | ||
protected static unsafe void WriteDirect(ref (IMemoryOwner<byte>, int) output, ReadOnlySpan<byte> bytes) => CustomCommandUtils.WriteDirect(ref output, bytes); | ||
|
||
/// <summary> | ||
/// Get argument from input, at specified offset (starting from 0) | ||
/// </summary> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
using System; | ||
using System.Buffers; | ||
using System.Text; | ||
|
||
namespace Garnet.server | ||
{ | ||
/// <summary> | ||
/// Provides extension methods for handling custom object output writing operations. | ||
/// </summary> | ||
public static class RespCustomObjectOutputWriterExtensions | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO I don't think extension method are the correct choice here, these can be added to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
{ | ||
/// <summary> | ||
/// Aborts the execution of the current object store command and outputs | ||
/// an error message to indicate a wrong number of arguments for the given command. | ||
/// </summary> | ||
/// <param name="cmdName">Name of the command that caused the error message.</param> | ||
/// <returns>true if the command was completely consumed, false if the input on the receive buffer was incomplete.</returns> | ||
public static bool AbortWithWrongNumberOfArguments(this ref (IMemoryOwner<byte>, int) output, string cmdName) | ||
{ | ||
var errorMessage = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, cmdName)); | ||
|
||
return output.AbortWithErrorMessage(errorMessage); | ||
} | ||
|
||
/// <summary> | ||
/// Aborts the execution of the current object store command and outputs a given error message. | ||
/// </summary> | ||
/// <param name="errorMessage">Error message to print to result stream.</param> | ||
/// <returns>true if the command was completely consumed, false if the input on the receive buffer was incomplete.</returns> | ||
public static bool AbortWithErrorMessage(this ref (IMemoryOwner<byte>, int) output, ReadOnlySpan<byte> errorMessage) | ||
{ | ||
CustomCommandUtils.WriteError(ref output, errorMessage); | ||
|
||
return true; | ||
} | ||
|
||
/// <summary> | ||
/// Aborts the execution of the current object store command and outputs a given error message. | ||
/// </summary> | ||
/// <param name="errorMessage">Error message to print to result stream.</param> | ||
/// <returns>true if the command was completely consumed, false if the input on the receive buffer was incomplete.</returns> | ||
public static bool AbortWithErrorMessage(this ref (IMemoryOwner<byte>, int) output, string errorMessage) | ||
{ | ||
CustomCommandUtils.WriteError(ref output, errorMessage); | ||
|
||
return true; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ namespace Garnet.server | |
/// <summary> | ||
/// Command strings for RESP protocol | ||
/// </summary> | ||
static partial class CmdStrings | ||
public static partial class CmdStrings | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not convinced that we should make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I didn't understand how adding the |
||
{ | ||
/// <summary> | ||
/// Request strings | ||
|
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.
General Q - where did these test cases come from? Just curious if this is copied from somewhere.
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 have asked Copilot to create sample JPaths for each type of JPath supported by Redis and Newtonsoft