• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Sholtee / json / 180

01 Aug 2024 03:36AM UTC coverage: 94.516% (+0.005%) from 94.511%
180

push

appveyor

Sholtee
improve JsonWriter.WriteString() performance

1827 of 1933 relevant lines covered (94.52%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.32
/SRC/Public/JsonWriter.cs
1
/********************************************************************************
2
* JsonWriter.cs                                                                 *
3
*                                                                               *
4
* Author: Denes Solti                                                           *
5
********************************************************************************/
6
using System;
7
using System.Diagnostics;
8
using System.Globalization;
9
using System.IO;
10
using System.Linq;
11
using System.Runtime.CompilerServices;
12
using System.Threading;
13

14
using static System.Char;
15

16
namespace Solti.Utils.Json
17
{
18
    using Internals;
19
    using Primitives;
20

21
    using static SerializationContext;
22
    using static Properties.Resources;
23

24
    /// <summary>
25
    /// Represents a low-level, cancellable JSON writer.
26
    /// </summary>
27
    /// <remarks>This class is thread safe.</remarks>
28
    public sealed class JsonWriter(int maxDepth = 64, byte indent = 2, int maxChunkSize = 1024)
1✔
29
    {
30
        #region Private
31
        private static readonly char[][] FSpaces = GetAllSpaces(256);
1✔
32

33
        private readonly char[] FValueSeparator = indent > 0 ? [' '] : [];
1✔
34

35
        private static char[][] GetAllSpaces(int maxLength)  // slow but will be called only once
36
        {
1✔
37
            char[][] spaces = new char[maxLength][];
1✔
38
            spaces[0] = [];
1✔
39
            for (int i = 1; i < maxLength; i++)
1✔
40
            {
1✔
41
                spaces[i] = GetSpacesAr(i);
1✔
42
            }
1✔
43
            return spaces;
1✔
44
        }
1✔
45

46
        private static char[] GetSpacesAr(int len) => [..Environment.NewLine, ..Enumerable.Repeat(' ', len)];
1✔
47

48
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
49
        private char[] GetSpaces(int val) => (val *= indent) < FSpaces.Length
×
50
            ? FSpaces[val]
×
51
            : GetSpacesAr(val);
×
52

53
        /// <summary>
54
        /// Validates then increases the <paramref name="currentDepth"/>. Throws an <see cref="InvalidOperationException"/> if the current depth reached the <see cref="maxDepth"/>.
55
        /// </summary>
56
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
57
        private int Deeper(int currentDepth)
58
        {
1✔
59
            if (++currentDepth > maxDepth)
1✔
60
                throw new JsonWriterException(MAX_DEPTH_REACHED);
1✔
61
            return currentDepth;
1✔
62
        }
1✔
63

64
        /// <summary>
65
        /// Verifies the given <paramref name="delegate"/>
66
        /// </summary>
67
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
68
        private static T VerifyDelegate<T>(T? @delegate) where T : Delegate => @delegate ?? throw new InvalidOperationException(INVALID_CONTEXT);
×
69
        #endregion
70

71
        #region Internal
72
        internal readonly ref struct Session(TextWriter dest, bool closeDest = true, in CancellationToken cancellation = default)
73
        {
74
            public readonly TextWriter Dest = dest;
1✔
75

76
            public readonly Buffer<char> Buffer = new(256);
1✔
77

78
            public readonly CancellationToken CancellationToken = cancellation;
1✔
79

80
            public void Dispose()
81
            {
1✔
82
                if (closeDest)
1✔
83
                    Dest.Dispose();
1✔
84
                Buffer.Dispose();
1✔
85
            }
1✔
86
        }
87

88
        /// <summary>
89
        /// Writes a JSON string to the underlying buffer representing the given <paramref name="str"/>.
90
        /// </summary>
91
        /// <remarks>If the given <paramref name="str"/> is not a <see cref="string"/> this method tries to convert it first.</remarks>
92
        internal void WriteString(in Session session, object str, SerializationContext currentContext, int currentDepth, char[]? explicitIndent)
93
        {
1✔
94
            ReadOnlySpan<char>
1✔
95
                s = str is string @string
1✔
96
                    ? @string.AsSpan()
1✔
97
                    : VerifyDelegate(currentContext.ConvertToString)(str, session.Buffer),
1✔
98
                commonChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxy0123456789-._".AsSpan();
1✔
99

100
            session.Dest.Write(explicitIndent ?? GetSpaces(currentDepth));
1✔
101
            session.Dest.Write('"');
1✔
102
    
103
            for (int pos = 0; pos < s.Length;)
1✔
104
            {
1✔
105
                ReadOnlySpan<char> charsLeft = s.Slice(pos);
1✔
106

107
                int len = Math.Min(charsLeft.Length, maxChunkSize);
1✔
108

109
                //
110
                // Skip the most common characters
111
                //
112

113
                for(int i = Math.Max(0, charsLeft.Slice(0, len).IndexOfAnyExcept(commonChars)); i < len; i++)
1✔
114
                {
1✔
115
                    switch (charsLeft[i])
1✔
116
                    {
117
                        case '"':
118
                            session.Dest.Write(charsLeft, 0, i);
1✔
119
                            pos += i + 1;
1✔
120
                            session.Dest.Write("\\\"");
1✔
121
                            goto nextChunk;
1✔
122
                        case '\r':
123
                            session.Dest.Write(charsLeft, 0, i);
1✔
124
                            pos += i + 1;
1✔
125
                            session.Dest.Write("\\r");
1✔
126
                            goto nextChunk;
1✔
127
                        case '\n':
128
                            session.Dest.Write(charsLeft, 0, i);
1✔
129
                            pos += i + 1;
1✔
130
                            session.Dest.Write("\\n");
1✔
131
                            goto nextChunk;
1✔
132
                        case '\\':
133
                            session.Dest.Write(charsLeft, 0, i);
1✔
134
                            pos += i + 1;
1✔
135
                            session.Dest.Write("\\\\");
1✔
136
                            goto nextChunk;
1✔
137
                        case '\b':
138
                            session.Dest.Write(charsLeft, 0, i);
1✔
139
                            pos += i + 1;
1✔
140
                            session.Dest.Write("\\b");
1✔
141
                            goto nextChunk;
1✔
142
                        case '\t':
143
                            session.Dest.Write(charsLeft, 0, i);
1✔
144
                            pos += i + 1;
1✔
145
                            session.Dest.Write("\\t");
1✔
146
                            goto nextChunk;
1✔
147
                        default:
148
                            byte escape;
149

150
                            if (IsControl(charsLeft[i]))
1✔
151
                                escape = 1;
1✔
152
                            else if (i < charsLeft.Length - 1 /*override "len"*/ && IsSurrogatePair(charsLeft[i], charsLeft[i + 1]))
1✔
153
                                escape = 2;
1✔
154
                            else
155
                                //
156
                                // TODO: We may skip common chars here as well
157
                                //
158

159
                                break;
1✔
160

161
                            session.Dest.Write(charsLeft, 0, i);
1✔
162
                            pos += i + escape;
1✔
163

164
                            for (byte j = 0; j < escape; j++, i++)
1✔
165
                            {
1✔
166
                                int ord = charsLeft[i];
1✔
167
                                session.Dest.Write("\\u");
1✔
168
                                session.Dest.Write(ord.ToString("X4", CultureInfo.InvariantCulture));
1✔
169
                            }
1✔
170

171
                            goto nextChunk;
1✔
172
                    }
173
                }
1✔
174

175
                session.Dest.Write(charsLeft, 0, len);
1✔
176
                pos += len;
1✔
177

178
                nextChunk:
1✔
179
                Debug.Assert(pos <= s.Length, "Miscalculated position");
1✔
180
            }
1✔
181

182
            session.Dest.Write('"');
1✔
183
        }
1✔
184

185
        /// <summary>
186
        /// Writes the given value to the underlying buffer.
187
        /// </summary>
188
        internal void WriteValue(in Session session, object? val, SerializationContext currentContext, int currentDepth, char[]? explicitIndent)
189
        {
1✔
190
            session.Dest.Write(explicitIndent ?? GetSpaces(currentDepth));
1✔
191
            session.Dest.Write
1✔
192
            (
1✔
193
                VerifyDelegate(currentContext.ConvertToString)(val, session.Buffer)
1✔
194
            );
1✔
195
        }
1✔
196

197
        /// <summary>
198
        /// Writes the list value to the underlying buffer.
199
        /// </summary>
200
        internal void WriteList(in Session session, object val, SerializationContext currentContext, int currentDepth, char[]? explicitIndent)
201
        {
1✔
202
            session.Dest.Write(explicitIndent ?? GetSpaces(currentDepth));
1✔
203
            session.Dest.Write('[');
1✔
204

205
            bool firstItem = true;
1✔
206
            foreach (Entry entry in VerifyDelegate(currentContext.EnumEntries)(val))
1✔
207
            {
1✔
208
                if (firstItem) firstItem = false;
1✔
209
                else session.Dest.Write(',');
1✔
210

211
                Write(in session, entry.Value, entry.Context, Deeper(currentDepth), null);
1✔
212
            }
1✔
213

214
            session.Dest.Write(GetSpaces(currentDepth));
1✔
215
            if (currentDepth is 0 && indent > 0)
1✔
216
                session.Dest.Write(Environment.NewLine);
1✔
217
            session.Dest.Write(']');
1✔
218
        }
1✔
219

220
        /// <summary>
221
        /// Writes the given object to the underlying buffer.
222
        /// </summary>
223
        internal void WriteObject(in Session session, object val, SerializationContext currentContext, int currentDepth, char[]? explicitIndent)
224
        {
1✔
225
            session.Dest.Write(explicitIndent ?? GetSpaces(currentDepth));
1✔
226
            session.Dest.Write('{');
1✔
227

228
            bool firstItem = true;
1✔
229
            foreach (Entry entry in VerifyDelegate(currentContext.EnumEntries)(val))
1✔
230
            {
1✔
231
                if (firstItem) firstItem = false;
1✔
232
                else session.Dest.Write(',');
1✔
233

234
                WriteString(in session, entry.Name!, entry.Context, Deeper(currentDepth), null);
1✔
235
                session.Dest.Write(':');
1✔
236
                Write(in session, entry.Value, entry.Context, Deeper(currentDepth), FValueSeparator);
1✔
237
            }
1✔
238

239
            session.Dest.Write(GetSpaces(currentDepth));
1✔
240
            if (currentDepth is 0 && indent > 0)
1✔
241
                session.Dest.Write(Environment.NewLine);
1✔
242
            session.Dest.Write('}');
1✔
243
        }
1✔
244

245
        internal void Write(in Session session, object? val, SerializationContext currentContext, int currentDepth, char[]? explicitIndent)
246
        {
1✔
247
            session.CancellationToken.ThrowIfCancellationRequested();
1✔
248

249
            switch (VerifyDelegate(currentContext.GetTypeOf)(val))
1✔
250
            {
251
                case JsonDataTypes.Number:
252
                case JsonDataTypes.Boolean:
253
                case JsonDataTypes.Null:
254
                    WriteValue(in session, val, currentContext, currentDepth, explicitIndent);
1✔
255
                    break;
1✔
256
                case JsonDataTypes.String:
257
                    WriteString(in session, val!, currentContext, currentDepth, explicitIndent);
1✔
258
                    break;
1✔
259
                case JsonDataTypes.List:
260
                    WriteList(in session, val!, currentContext, currentDepth, explicitIndent);
1✔
261
                    break;
1✔
262
                case JsonDataTypes.Object:
263
                    WriteObject(in session, val!, currentContext, currentDepth, explicitIndent);
1✔
264
                    break;
1✔
265
                default:
266
                    throw new JsonWriterException(NOT_SERIALIZABLE);
1✔
267
            }
268
        }
1✔
269
        #endregion
270

271
        /// <summary>
272
        /// Serializes the given <paramref name="value"/>.
273
        /// </summary>
274
        /// <param name="dest">The destination that holds the serialized content.</param>
275
        /// <param name="closeDest">If set to true, the system disposes the <paramref name="dest"/> before this function would return.</param>
276
        /// <param name="value">Value to be serialized</param>
277
        /// <param name="context">Context that instructs the system how to serialize the input.</param>
278
        /// <param name="cancellation"><see cref="CancellationToken"/></param>
279
        public void Write(TextWriter dest, bool closeDest, object? value, SerializationContext context, in CancellationToken cancellation)
280
        {
1✔
281
            if (dest is null)
1✔
282
                throw new ArgumentNullException(nameof(dest));
1✔
283

284
            using Session session = new(dest, closeDest, in cancellation);
1✔
285
            Write(in session, value, context, 0, null);
1✔
286
        }
1✔
287
    }
288
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc