Fix Apply in PrimitiveColumnContainer so it does not change source column - #6642

Merged
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main
May 5, 2023
Merged

Fix Apply in PrimitiveColumnContainer so it does not change source column#6642
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main

Conversation

@janholo

Copy link
Copy Markdown
Contributor

The Apply method calls SetValidityBit on the source container and not on the target container. That way the null check of the container values gets incorrect.

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

@janholo

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

@codecov

codecovBot commented May 2, 2023

Copy link
Copy Markdown

Codecov Report

Merging #6642 (53b90ae) into main (a18b9cb) will decrease coverage by 0.01%.
The diff coverage is 83.33%.

Additional details and impacted files
@@ Coverage Diff @@## main #6642 +/- ##
==========================================
- Coverage 68.53% 68.52% -0.01% 
==========================================
Files 1200 1200 Lines 250287 250279 -8 Branches 26093 26093 ==========================================
- Hits 171526 171509 -17 - Misses 71929 71936 +7 - Partials 6832 6834 +2 
FlagCoverage Δ
Debug68.52% <83.33%> (-0.01%)⬇️
production63.00% <83.33%> (-0.01%)⬇️
test88.83% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...icrosoft.Data.Analysis/PrimitiveColumnContainer.cs83.01% <83.33%> (-0.20%)⬇️

... and 2 files with indirect coverage changes

@JakeRadMSFT

JakeRadMSFT commented May 4, 2023

Copy link
Copy Markdown
Member

@janholo Thanks for your PR!

Could you add a test that fails before your change and passes after?

It would help me better understand the problem and it's solution.

Comment threadsrc/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs

@JakeRadMSFTJakeRadMSFT left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I dug into this a bunch to try and understand the differences and overlap. After doing that investigation - I like your change. It fixes 2 things.

What Does Apply Do?

The goal of Apply is to start with the value from this container ... call func with the value or default for the type ... then assign the result to resultContainer. Since it's possible the func changed a value from null to not null or vice-versa we have to update the validityBit.

Thing 1

This method used to get mutable versions of span and nullBitMap for the SourceContainer or this that we're not changing.

Your change makes it so we don't create mutable versions of source ... since it's not needed. Nothing should change with source.

Thing 2

It also fixes a bug where we were setting the validity bit on the SourceContainer because we were calling this.SetValidityBit instead of resultContainer.SetValidityBit.

DataFrameBuffer<byte> mutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(NullBitMapBuffers[b]);
NullBitMapBuffers[b] = mutableNullBitMapBuffer;
Span<byte> nullBitMapSpan = mutableNullBitMapBuffer.Span;
ReadOnlyDataFrameBuffer<T> sourceBuffer = Buffers[b];

@JakeRadMSFTJakeRadMSFTMay 4, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke@jaredpar

Just for my own learning purposes. Is this something that would be improved with the span changes in .NET? Would we no longer need to do all the GetMutableBuffer stuff?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically I'm wondering if there would be a better way to refactor out the code like this:

DataFrameBuffer<TResult> resultMutableBuffer = DataFrameBuffer<TResult>.GetMutableBuffer(resultBuffer);
resultContainer.Buffers[b] = resultMutableBuffer;
Span<TResult> resultSpan = resultMutableBuffer.Span;
DataFrameBuffer<byte> resultMutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(resultContainer.NullBitMapBuffers[b]);
resultContainer.NullBitMapBuffers[b] = resultMutableNullBitMapBuffer;
Span<byte> resultNullBitMapSpan = resultMutableNullBitMapBuffer.Span;

It's all over this file in some form.
Clone, Apply, ApplyElementWise, CloneAs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of very questionable stuff going on here. The code is going to great lengths to copy the original buffer to get a mutable version... and then passing the span to IsValid or calling the indexer, which both accept ReadOnlySpan.

And then there's the ReadOnlyDataFrameBuffer<T> type which takes a T : struct, but should be T : unmanaged.

This code could likely be simplified by extracting some helper methods, but I think the above issues indicate that someone should review the actual intent and ensure that the code reflects it.

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sort of what it felt like but I appreciate having some expert eyes on it! Thanks @agocke!

I might take this up as a pet project in the future :)

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke - I'm not sure it's 100% of the way there but I did a pass and opened a different PR.

https://github.com/dotnet/machinelearning/pull/6656/files

@asmirnov82asmirnov82May 29, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that PR already closed, but I have some comments regarding this GetMutableBuffer staff. It looks it's used for working with readonly memory, when column is created from the Apache Arrow RecordBatch in

public static DataFrame FromArrowRecordBatch(RecordBatch recordBatch)
{
DataFrame ret = new DataFrame();
Apache.Arrow.Schema arrowSchema = recordBatch.Schema;
int fieldIndex = 0;
IEnumerable<IArrowArray> arrowArrays = recordBatch.Arrays;
foreach (IArrowArray arrowArray in arrowArrays)
{
Field field = arrowSchema.GetFieldByIndex(fieldIndex);
AppendDataFrameColumnFromArrowArray(field, arrowArray, ret);
fieldIndex++;
}
return ret;
}

However, this implementation looks very suspicious.

RecordBatch is a disposable object. Apache Arrow by default uses NativeMemoryAllocator to allocate unmanaged memory (for example, this default allocator is used in Spark.Net to create RecorBatch and pass it to DataFrame.FromArrowRecordBatch factory method).
So it's up to a DataFrame to hold the link to the RecordBatch and correctly Dispose it. Or we have to copy the unmanaged readonly memory from the RecordBatch into managed buffers (that exactly what is happening in GetMutableBuffer on attempt to edit data), but in this case we can avoid using ReadOnlyBuffers at all.

@michaelgsharp
michaelgsharp merged commit 33342a2 into dotnet:mainMay 5, 2023
@janholo

janholo commented May 11, 2023

Copy link
Copy Markdown
ContributorAuthor

Hey @JakeRadMSFT, thanks for taking the time to review and merge the PR.

Sorry for not providing tests. What I still think is strange is that Apply and ApplyElementwise have similar signatures but do fundamentally different things. One changes "this" column and the other operates on an other column.

@ghostghost locked as resolved and limited conversation to collaborators Jun 28, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janholo@JakeRadMSFT@agocke@asmirnov82@michaelgsharp@jan-rhim
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix Apply in PrimitiveColumnContainer so it does not change source column - #6642

Merged
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main
May 5, 2023
Merged

Fix Apply in PrimitiveColumnContainer so it does not change source column#6642
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main

Conversation

@janholo

Copy link
Copy Markdown
Contributor

The Apply method calls SetValidityBit on the source container and not on the target container. That way the null check of the container values gets incorrect.

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

@janholo

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

@codecov

codecovBot commented May 2, 2023

Copy link
Copy Markdown

Codecov Report

Merging #6642 (53b90ae) into main (a18b9cb) will decrease coverage by 0.01%.
The diff coverage is 83.33%.

Additional details and impacted files
@@ Coverage Diff @@## main #6642 +/- ##
==========================================
- Coverage 68.53% 68.52% -0.01% 
==========================================
Files 1200 1200 Lines 250287 250279 -8 Branches 26093 26093 ==========================================
- Hits 171526 171509 -17 - Misses 71929 71936 +7 - Partials 6832 6834 +2 
FlagCoverage Δ
Debug68.52% <83.33%> (-0.01%)⬇️
production63.00% <83.33%> (-0.01%)⬇️
test88.83% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...icrosoft.Data.Analysis/PrimitiveColumnContainer.cs83.01% <83.33%> (-0.20%)⬇️

... and 2 files with indirect coverage changes

@JakeRadMSFT

JakeRadMSFT commented May 4, 2023

Copy link
Copy Markdown
Member

@janholo Thanks for your PR!

Could you add a test that fails before your change and passes after?

It would help me better understand the problem and it's solution.

Comment threadsrc/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs

@JakeRadMSFTJakeRadMSFT left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I dug into this a bunch to try and understand the differences and overlap. After doing that investigation - I like your change. It fixes 2 things.

What Does Apply Do?

The goal of Apply is to start with the value from this container ... call func with the value or default for the type ... then assign the result to resultContainer. Since it's possible the func changed a value from null to not null or vice-versa we have to update the validityBit.

Thing 1

This method used to get mutable versions of span and nullBitMap for the SourceContainer or this that we're not changing.

Your change makes it so we don't create mutable versions of source ... since it's not needed. Nothing should change with source.

Thing 2

It also fixes a bug where we were setting the validity bit on the SourceContainer because we were calling this.SetValidityBit instead of resultContainer.SetValidityBit.

DataFrameBuffer<byte> mutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(NullBitMapBuffers[b]);
NullBitMapBuffers[b] = mutableNullBitMapBuffer;
Span<byte> nullBitMapSpan = mutableNullBitMapBuffer.Span;
ReadOnlyDataFrameBuffer<T> sourceBuffer = Buffers[b];

@JakeRadMSFTJakeRadMSFTMay 4, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke@jaredpar

Just for my own learning purposes. Is this something that would be improved with the span changes in .NET? Would we no longer need to do all the GetMutableBuffer stuff?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically I'm wondering if there would be a better way to refactor out the code like this:

DataFrameBuffer<TResult> resultMutableBuffer = DataFrameBuffer<TResult>.GetMutableBuffer(resultBuffer);
resultContainer.Buffers[b] = resultMutableBuffer;
Span<TResult> resultSpan = resultMutableBuffer.Span;
DataFrameBuffer<byte> resultMutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(resultContainer.NullBitMapBuffers[b]);
resultContainer.NullBitMapBuffers[b] = resultMutableNullBitMapBuffer;
Span<byte> resultNullBitMapSpan = resultMutableNullBitMapBuffer.Span;

It's all over this file in some form.
Clone, Apply, ApplyElementWise, CloneAs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of very questionable stuff going on here. The code is going to great lengths to copy the original buffer to get a mutable version... and then passing the span to IsValid or calling the indexer, which both accept ReadOnlySpan.

And then there's the ReadOnlyDataFrameBuffer<T> type which takes a T : struct, but should be T : unmanaged.

This code could likely be simplified by extracting some helper methods, but I think the above issues indicate that someone should review the actual intent and ensure that the code reflects it.

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sort of what it felt like but I appreciate having some expert eyes on it! Thanks @agocke!

I might take this up as a pet project in the future :)

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke - I'm not sure it's 100% of the way there but I did a pass and opened a different PR.

https://github.com/dotnet/machinelearning/pull/6656/files

@asmirnov82asmirnov82May 29, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that PR already closed, but I have some comments regarding this GetMutableBuffer staff. It looks it's used for working with readonly memory, when column is created from the Apache Arrow RecordBatch in

public static DataFrame FromArrowRecordBatch(RecordBatch recordBatch)
{
DataFrame ret = new DataFrame();
Apache.Arrow.Schema arrowSchema = recordBatch.Schema;
int fieldIndex = 0;
IEnumerable<IArrowArray> arrowArrays = recordBatch.Arrays;
foreach (IArrowArray arrowArray in arrowArrays)
{
Field field = arrowSchema.GetFieldByIndex(fieldIndex);
AppendDataFrameColumnFromArrowArray(field, arrowArray, ret);
fieldIndex++;
}
return ret;
}

However, this implementation looks very suspicious.

RecordBatch is a disposable object. Apache Arrow by default uses NativeMemoryAllocator to allocate unmanaged memory (for example, this default allocator is used in Spark.Net to create RecorBatch and pass it to DataFrame.FromArrowRecordBatch factory method).
So it's up to a DataFrame to hold the link to the RecordBatch and correctly Dispose it. Or we have to copy the unmanaged readonly memory from the RecordBatch into managed buffers (that exactly what is happening in GetMutableBuffer on attempt to edit data), but in this case we can avoid using ReadOnlyBuffers at all.

@michaelgsharp
michaelgsharp merged commit 33342a2 into dotnet:mainMay 5, 2023
@janholo

janholo commented May 11, 2023

Copy link
Copy Markdown
ContributorAuthor

Hey @JakeRadMSFT, thanks for taking the time to review and merge the PR.

Sorry for not providing tests. What I still think is strange is that Apply and ApplyElementwise have similar signatures but do fundamentally different things. One changes "this" column and the other operates on an other column.

@ghostghost locked as resolved and limited conversation to collaborators Jun 28, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janholo@JakeRadMSFT@agocke@asmirnov82@michaelgsharp@jan-rhim
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix Apply in PrimitiveColumnContainer so it does not change source column - #6642

Merged
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main
May 5, 2023
Merged

Fix Apply in PrimitiveColumnContainer so it does not change source column#6642
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main

Conversation

@janholo

Copy link
Copy Markdown
Contributor

The Apply method calls SetValidityBit on the source container and not on the target container. That way the null check of the container values gets incorrect.

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

@janholo

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

@codecov

codecovBot commented May 2, 2023

Copy link
Copy Markdown

Codecov Report

Merging #6642 (53b90ae) into main (a18b9cb) will decrease coverage by 0.01%.
The diff coverage is 83.33%.

Additional details and impacted files
@@ Coverage Diff @@## main #6642 +/- ##
==========================================
- Coverage 68.53% 68.52% -0.01% 
==========================================
Files 1200 1200 Lines 250287 250279 -8 Branches 26093 26093 ==========================================
- Hits 171526 171509 -17 - Misses 71929 71936 +7 - Partials 6832 6834 +2 
FlagCoverage Δ
Debug68.52% <83.33%> (-0.01%)⬇️
production63.00% <83.33%> (-0.01%)⬇️
test88.83% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...icrosoft.Data.Analysis/PrimitiveColumnContainer.cs83.01% <83.33%> (-0.20%)⬇️

... and 2 files with indirect coverage changes

@JakeRadMSFT

JakeRadMSFT commented May 4, 2023

Copy link
Copy Markdown
Member

@janholo Thanks for your PR!

Could you add a test that fails before your change and passes after?

It would help me better understand the problem and it's solution.

Comment threadsrc/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs

@JakeRadMSFTJakeRadMSFT left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I dug into this a bunch to try and understand the differences and overlap. After doing that investigation - I like your change. It fixes 2 things.

What Does Apply Do?

The goal of Apply is to start with the value from this container ... call func with the value or default for the type ... then assign the result to resultContainer. Since it's possible the func changed a value from null to not null or vice-versa we have to update the validityBit.

Thing 1

This method used to get mutable versions of span and nullBitMap for the SourceContainer or this that we're not changing.

Your change makes it so we don't create mutable versions of source ... since it's not needed. Nothing should change with source.

Thing 2

It also fixes a bug where we were setting the validity bit on the SourceContainer because we were calling this.SetValidityBit instead of resultContainer.SetValidityBit.

DataFrameBuffer<byte> mutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(NullBitMapBuffers[b]);
NullBitMapBuffers[b] = mutableNullBitMapBuffer;
Span<byte> nullBitMapSpan = mutableNullBitMapBuffer.Span;
ReadOnlyDataFrameBuffer<T> sourceBuffer = Buffers[b];

@JakeRadMSFTJakeRadMSFTMay 4, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke@jaredpar

Just for my own learning purposes. Is this something that would be improved with the span changes in .NET? Would we no longer need to do all the GetMutableBuffer stuff?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically I'm wondering if there would be a better way to refactor out the code like this:

DataFrameBuffer<TResult> resultMutableBuffer = DataFrameBuffer<TResult>.GetMutableBuffer(resultBuffer);
resultContainer.Buffers[b] = resultMutableBuffer;
Span<TResult> resultSpan = resultMutableBuffer.Span;
DataFrameBuffer<byte> resultMutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(resultContainer.NullBitMapBuffers[b]);
resultContainer.NullBitMapBuffers[b] = resultMutableNullBitMapBuffer;
Span<byte> resultNullBitMapSpan = resultMutableNullBitMapBuffer.Span;

It's all over this file in some form.
Clone, Apply, ApplyElementWise, CloneAs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of very questionable stuff going on here. The code is going to great lengths to copy the original buffer to get a mutable version... and then passing the span to IsValid or calling the indexer, which both accept ReadOnlySpan.

And then there's the ReadOnlyDataFrameBuffer<T> type which takes a T : struct, but should be T : unmanaged.

This code could likely be simplified by extracting some helper methods, but I think the above issues indicate that someone should review the actual intent and ensure that the code reflects it.

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sort of what it felt like but I appreciate having some expert eyes on it! Thanks @agocke!

I might take this up as a pet project in the future :)

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke - I'm not sure it's 100% of the way there but I did a pass and opened a different PR.

https://github.com/dotnet/machinelearning/pull/6656/files

@asmirnov82asmirnov82May 29, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that PR already closed, but I have some comments regarding this GetMutableBuffer staff. It looks it's used for working with readonly memory, when column is created from the Apache Arrow RecordBatch in

public static DataFrame FromArrowRecordBatch(RecordBatch recordBatch)
{
DataFrame ret = new DataFrame();
Apache.Arrow.Schema arrowSchema = recordBatch.Schema;
int fieldIndex = 0;
IEnumerable<IArrowArray> arrowArrays = recordBatch.Arrays;
foreach (IArrowArray arrowArray in arrowArrays)
{
Field field = arrowSchema.GetFieldByIndex(fieldIndex);
AppendDataFrameColumnFromArrowArray(field, arrowArray, ret);
fieldIndex++;
}
return ret;
}

However, this implementation looks very suspicious.

RecordBatch is a disposable object. Apache Arrow by default uses NativeMemoryAllocator to allocate unmanaged memory (for example, this default allocator is used in Spark.Net to create RecorBatch and pass it to DataFrame.FromArrowRecordBatch factory method).
So it's up to a DataFrame to hold the link to the RecordBatch and correctly Dispose it. Or we have to copy the unmanaged readonly memory from the RecordBatch into managed buffers (that exactly what is happening in GetMutableBuffer on attempt to edit data), but in this case we can avoid using ReadOnlyBuffers at all.

@michaelgsharp
michaelgsharp merged commit 33342a2 into dotnet:mainMay 5, 2023
@janholo

janholo commented May 11, 2023

Copy link
Copy Markdown
ContributorAuthor

Hey @JakeRadMSFT, thanks for taking the time to review and merge the PR.

Sorry for not providing tests. What I still think is strange is that Apply and ApplyElementwise have similar signatures but do fundamentally different things. One changes "this" column and the other operates on an other column.

@ghostghost locked as resolved and limited conversation to collaborators Jun 28, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janholo@JakeRadMSFT@agocke@asmirnov82@michaelgsharp@jan-rhim
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix Apply in PrimitiveColumnContainer so it does not change source column - #6642

Merged
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main
May 5, 2023
Merged

Fix Apply in PrimitiveColumnContainer so it does not change source column#6642
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main

Conversation

@janholo

Copy link
Copy Markdown
Contributor

The Apply method calls SetValidityBit on the source container and not on the target container. That way the null check of the container values gets incorrect.

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

@janholo

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

@codecov

codecovBot commented May 2, 2023

Copy link
Copy Markdown

Codecov Report

Merging #6642 (53b90ae) into main (a18b9cb) will decrease coverage by 0.01%.
The diff coverage is 83.33%.

Additional details and impacted files
@@ Coverage Diff @@## main #6642 +/- ##
==========================================
- Coverage 68.53% 68.52% -0.01% 
==========================================
Files 1200 1200 Lines 250287 250279 -8 Branches 26093 26093 ==========================================
- Hits 171526 171509 -17 - Misses 71929 71936 +7 - Partials 6832 6834 +2 
FlagCoverage Δ
Debug68.52% <83.33%> (-0.01%)⬇️
production63.00% <83.33%> (-0.01%)⬇️
test88.83% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...icrosoft.Data.Analysis/PrimitiveColumnContainer.cs83.01% <83.33%> (-0.20%)⬇️

... and 2 files with indirect coverage changes

@JakeRadMSFT

JakeRadMSFT commented May 4, 2023

Copy link
Copy Markdown
Member

@janholo Thanks for your PR!

Could you add a test that fails before your change and passes after?

It would help me better understand the problem and it's solution.

Comment threadsrc/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs

@JakeRadMSFTJakeRadMSFT left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I dug into this a bunch to try and understand the differences and overlap. After doing that investigation - I like your change. It fixes 2 things.

What Does Apply Do?

The goal of Apply is to start with the value from this container ... call func with the value or default for the type ... then assign the result to resultContainer. Since it's possible the func changed a value from null to not null or vice-versa we have to update the validityBit.

Thing 1

This method used to get mutable versions of span and nullBitMap for the SourceContainer or this that we're not changing.

Your change makes it so we don't create mutable versions of source ... since it's not needed. Nothing should change with source.

Thing 2

It also fixes a bug where we were setting the validity bit on the SourceContainer because we were calling this.SetValidityBit instead of resultContainer.SetValidityBit.

DataFrameBuffer<byte> mutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(NullBitMapBuffers[b]);
NullBitMapBuffers[b] = mutableNullBitMapBuffer;
Span<byte> nullBitMapSpan = mutableNullBitMapBuffer.Span;
ReadOnlyDataFrameBuffer<T> sourceBuffer = Buffers[b];

@JakeRadMSFTJakeRadMSFTMay 4, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke@jaredpar

Just for my own learning purposes. Is this something that would be improved with the span changes in .NET? Would we no longer need to do all the GetMutableBuffer stuff?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically I'm wondering if there would be a better way to refactor out the code like this:

DataFrameBuffer<TResult> resultMutableBuffer = DataFrameBuffer<TResult>.GetMutableBuffer(resultBuffer);
resultContainer.Buffers[b] = resultMutableBuffer;
Span<TResult> resultSpan = resultMutableBuffer.Span;
DataFrameBuffer<byte> resultMutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(resultContainer.NullBitMapBuffers[b]);
resultContainer.NullBitMapBuffers[b] = resultMutableNullBitMapBuffer;
Span<byte> resultNullBitMapSpan = resultMutableNullBitMapBuffer.Span;

It's all over this file in some form.
Clone, Apply, ApplyElementWise, CloneAs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of very questionable stuff going on here. The code is going to great lengths to copy the original buffer to get a mutable version... and then passing the span to IsValid or calling the indexer, which both accept ReadOnlySpan.

And then there's the ReadOnlyDataFrameBuffer<T> type which takes a T : struct, but should be T : unmanaged.

This code could likely be simplified by extracting some helper methods, but I think the above issues indicate that someone should review the actual intent and ensure that the code reflects it.

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sort of what it felt like but I appreciate having some expert eyes on it! Thanks @agocke!

I might take this up as a pet project in the future :)

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke - I'm not sure it's 100% of the way there but I did a pass and opened a different PR.

https://github.com/dotnet/machinelearning/pull/6656/files

@asmirnov82asmirnov82May 29, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that PR already closed, but I have some comments regarding this GetMutableBuffer staff. It looks it's used for working with readonly memory, when column is created from the Apache Arrow RecordBatch in

public static DataFrame FromArrowRecordBatch(RecordBatch recordBatch)
{
DataFrame ret = new DataFrame();
Apache.Arrow.Schema arrowSchema = recordBatch.Schema;
int fieldIndex = 0;
IEnumerable<IArrowArray> arrowArrays = recordBatch.Arrays;
foreach (IArrowArray arrowArray in arrowArrays)
{
Field field = arrowSchema.GetFieldByIndex(fieldIndex);
AppendDataFrameColumnFromArrowArray(field, arrowArray, ret);
fieldIndex++;
}
return ret;
}

However, this implementation looks very suspicious.

RecordBatch is a disposable object. Apache Arrow by default uses NativeMemoryAllocator to allocate unmanaged memory (for example, this default allocator is used in Spark.Net to create RecorBatch and pass it to DataFrame.FromArrowRecordBatch factory method).
So it's up to a DataFrame to hold the link to the RecordBatch and correctly Dispose it. Or we have to copy the unmanaged readonly memory from the RecordBatch into managed buffers (that exactly what is happening in GetMutableBuffer on attempt to edit data), but in this case we can avoid using ReadOnlyBuffers at all.

@michaelgsharp
michaelgsharp merged commit 33342a2 into dotnet:mainMay 5, 2023
@janholo

janholo commented May 11, 2023

Copy link
Copy Markdown
ContributorAuthor

Hey @JakeRadMSFT, thanks for taking the time to review and merge the PR.

Sorry for not providing tests. What I still think is strange is that Apply and ApplyElementwise have similar signatures but do fundamentally different things. One changes "this" column and the other operates on an other column.

@ghostghost locked as resolved and limited conversation to collaborators Jun 28, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janholo@JakeRadMSFT@agocke@asmirnov82@michaelgsharp@jan-rhim
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix Apply in PrimitiveColumnContainer so it does not change source column - #6642

Merged
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main
May 5, 2023
Merged

Fix Apply in PrimitiveColumnContainer so it does not change source column#6642
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main

Conversation

@janholo

Copy link
Copy Markdown
Contributor

The Apply method calls SetValidityBit on the source container and not on the target container. That way the null check of the container values gets incorrect.

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

@janholo

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

@codecov

codecovBot commented May 2, 2023

Copy link
Copy Markdown

Codecov Report

Merging #6642 (53b90ae) into main (a18b9cb) will decrease coverage by 0.01%.
The diff coverage is 83.33%.

Additional details and impacted files
@@ Coverage Diff @@## main #6642 +/- ##
==========================================
- Coverage 68.53% 68.52% -0.01% 
==========================================
Files 1200 1200 Lines 250287 250279 -8 Branches 26093 26093 ==========================================
- Hits 171526 171509 -17 - Misses 71929 71936 +7 - Partials 6832 6834 +2 
FlagCoverage Δ
Debug68.52% <83.33%> (-0.01%)⬇️
production63.00% <83.33%> (-0.01%)⬇️
test88.83% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...icrosoft.Data.Analysis/PrimitiveColumnContainer.cs83.01% <83.33%> (-0.20%)⬇️

... and 2 files with indirect coverage changes

@JakeRadMSFT

JakeRadMSFT commented May 4, 2023

Copy link
Copy Markdown
Member

@janholo Thanks for your PR!

Could you add a test that fails before your change and passes after?

It would help me better understand the problem and it's solution.

Comment threadsrc/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs

@JakeRadMSFTJakeRadMSFT left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I dug into this a bunch to try and understand the differences and overlap. After doing that investigation - I like your change. It fixes 2 things.

What Does Apply Do?

The goal of Apply is to start with the value from this container ... call func with the value or default for the type ... then assign the result to resultContainer. Since it's possible the func changed a value from null to not null or vice-versa we have to update the validityBit.

Thing 1

This method used to get mutable versions of span and nullBitMap for the SourceContainer or this that we're not changing.

Your change makes it so we don't create mutable versions of source ... since it's not needed. Nothing should change with source.

Thing 2

It also fixes a bug where we were setting the validity bit on the SourceContainer because we were calling this.SetValidityBit instead of resultContainer.SetValidityBit.

DataFrameBuffer<byte> mutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(NullBitMapBuffers[b]);
NullBitMapBuffers[b] = mutableNullBitMapBuffer;
Span<byte> nullBitMapSpan = mutableNullBitMapBuffer.Span;
ReadOnlyDataFrameBuffer<T> sourceBuffer = Buffers[b];

@JakeRadMSFTJakeRadMSFTMay 4, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke@jaredpar

Just for my own learning purposes. Is this something that would be improved with the span changes in .NET? Would we no longer need to do all the GetMutableBuffer stuff?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically I'm wondering if there would be a better way to refactor out the code like this:

DataFrameBuffer<TResult> resultMutableBuffer = DataFrameBuffer<TResult>.GetMutableBuffer(resultBuffer);
resultContainer.Buffers[b] = resultMutableBuffer;
Span<TResult> resultSpan = resultMutableBuffer.Span;
DataFrameBuffer<byte> resultMutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(resultContainer.NullBitMapBuffers[b]);
resultContainer.NullBitMapBuffers[b] = resultMutableNullBitMapBuffer;
Span<byte> resultNullBitMapSpan = resultMutableNullBitMapBuffer.Span;

It's all over this file in some form.
Clone, Apply, ApplyElementWise, CloneAs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of very questionable stuff going on here. The code is going to great lengths to copy the original buffer to get a mutable version... and then passing the span to IsValid or calling the indexer, which both accept ReadOnlySpan.

And then there's the ReadOnlyDataFrameBuffer<T> type which takes a T : struct, but should be T : unmanaged.

This code could likely be simplified by extracting some helper methods, but I think the above issues indicate that someone should review the actual intent and ensure that the code reflects it.

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sort of what it felt like but I appreciate having some expert eyes on it! Thanks @agocke!

I might take this up as a pet project in the future :)

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke - I'm not sure it's 100% of the way there but I did a pass and opened a different PR.

https://github.com/dotnet/machinelearning/pull/6656/files

@asmirnov82asmirnov82May 29, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that PR already closed, but I have some comments regarding this GetMutableBuffer staff. It looks it's used for working with readonly memory, when column is created from the Apache Arrow RecordBatch in

public static DataFrame FromArrowRecordBatch(RecordBatch recordBatch)
{
DataFrame ret = new DataFrame();
Apache.Arrow.Schema arrowSchema = recordBatch.Schema;
int fieldIndex = 0;
IEnumerable<IArrowArray> arrowArrays = recordBatch.Arrays;
foreach (IArrowArray arrowArray in arrowArrays)
{
Field field = arrowSchema.GetFieldByIndex(fieldIndex);
AppendDataFrameColumnFromArrowArray(field, arrowArray, ret);
fieldIndex++;
}
return ret;
}

However, this implementation looks very suspicious.

RecordBatch is a disposable object. Apache Arrow by default uses NativeMemoryAllocator to allocate unmanaged memory (for example, this default allocator is used in Spark.Net to create RecorBatch and pass it to DataFrame.FromArrowRecordBatch factory method).
So it's up to a DataFrame to hold the link to the RecordBatch and correctly Dispose it. Or we have to copy the unmanaged readonly memory from the RecordBatch into managed buffers (that exactly what is happening in GetMutableBuffer on attempt to edit data), but in this case we can avoid using ReadOnlyBuffers at all.

@michaelgsharp
michaelgsharp merged commit 33342a2 into dotnet:mainMay 5, 2023
@janholo

janholo commented May 11, 2023

Copy link
Copy Markdown
ContributorAuthor

Hey @JakeRadMSFT, thanks for taking the time to review and merge the PR.

Sorry for not providing tests. What I still think is strange is that Apply and ApplyElementwise have similar signatures but do fundamentally different things. One changes "this" column and the other operates on an other column.

@ghostghost locked as resolved and limited conversation to collaborators Jun 28, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janholo@JakeRadMSFT@agocke@asmirnov82@michaelgsharp@jan-rhim
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix Apply in PrimitiveColumnContainer so it does not change source column - #6642

Merged
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main
May 5, 2023
Merged

Fix Apply in PrimitiveColumnContainer so it does not change source column#6642
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main

Conversation

@janholo

Copy link
Copy Markdown
Contributor

The Apply method calls SetValidityBit on the source container and not on the target container. That way the null check of the container values gets incorrect.

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

@janholo

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

@codecov

codecovBot commented May 2, 2023

Copy link
Copy Markdown

Codecov Report

Merging #6642 (53b90ae) into main (a18b9cb) will decrease coverage by 0.01%.
The diff coverage is 83.33%.

Additional details and impacted files
@@ Coverage Diff @@## main #6642 +/- ##
==========================================
- Coverage 68.53% 68.52% -0.01% 
==========================================
Files 1200 1200 Lines 250287 250279 -8 Branches 26093 26093 ==========================================
- Hits 171526 171509 -17 - Misses 71929 71936 +7 - Partials 6832 6834 +2 
FlagCoverage Δ
Debug68.52% <83.33%> (-0.01%)⬇️
production63.00% <83.33%> (-0.01%)⬇️
test88.83% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...icrosoft.Data.Analysis/PrimitiveColumnContainer.cs83.01% <83.33%> (-0.20%)⬇️

... and 2 files with indirect coverage changes

@JakeRadMSFT

JakeRadMSFT commented May 4, 2023

Copy link
Copy Markdown
Member

@janholo Thanks for your PR!

Could you add a test that fails before your change and passes after?

It would help me better understand the problem and it's solution.

Comment threadsrc/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs

@JakeRadMSFTJakeRadMSFT left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I dug into this a bunch to try and understand the differences and overlap. After doing that investigation - I like your change. It fixes 2 things.

What Does Apply Do?

The goal of Apply is to start with the value from this container ... call func with the value or default for the type ... then assign the result to resultContainer. Since it's possible the func changed a value from null to not null or vice-versa we have to update the validityBit.

Thing 1

This method used to get mutable versions of span and nullBitMap for the SourceContainer or this that we're not changing.

Your change makes it so we don't create mutable versions of source ... since it's not needed. Nothing should change with source.

Thing 2

It also fixes a bug where we were setting the validity bit on the SourceContainer because we were calling this.SetValidityBit instead of resultContainer.SetValidityBit.

DataFrameBuffer<byte> mutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(NullBitMapBuffers[b]);
NullBitMapBuffers[b] = mutableNullBitMapBuffer;
Span<byte> nullBitMapSpan = mutableNullBitMapBuffer.Span;
ReadOnlyDataFrameBuffer<T> sourceBuffer = Buffers[b];

@JakeRadMSFTJakeRadMSFTMay 4, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke@jaredpar

Just for my own learning purposes. Is this something that would be improved with the span changes in .NET? Would we no longer need to do all the GetMutableBuffer stuff?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically I'm wondering if there would be a better way to refactor out the code like this:

DataFrameBuffer<TResult> resultMutableBuffer = DataFrameBuffer<TResult>.GetMutableBuffer(resultBuffer);
resultContainer.Buffers[b] = resultMutableBuffer;
Span<TResult> resultSpan = resultMutableBuffer.Span;
DataFrameBuffer<byte> resultMutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(resultContainer.NullBitMapBuffers[b]);
resultContainer.NullBitMapBuffers[b] = resultMutableNullBitMapBuffer;
Span<byte> resultNullBitMapSpan = resultMutableNullBitMapBuffer.Span;

It's all over this file in some form.
Clone, Apply, ApplyElementWise, CloneAs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of very questionable stuff going on here. The code is going to great lengths to copy the original buffer to get a mutable version... and then passing the span to IsValid or calling the indexer, which both accept ReadOnlySpan.

And then there's the ReadOnlyDataFrameBuffer<T> type which takes a T : struct, but should be T : unmanaged.

This code could likely be simplified by extracting some helper methods, but I think the above issues indicate that someone should review the actual intent and ensure that the code reflects it.

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sort of what it felt like but I appreciate having some expert eyes on it! Thanks @agocke!

I might take this up as a pet project in the future :)

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke - I'm not sure it's 100% of the way there but I did a pass and opened a different PR.

https://github.com/dotnet/machinelearning/pull/6656/files

@asmirnov82asmirnov82May 29, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that PR already closed, but I have some comments regarding this GetMutableBuffer staff. It looks it's used for working with readonly memory, when column is created from the Apache Arrow RecordBatch in

public static DataFrame FromArrowRecordBatch(RecordBatch recordBatch)
{
DataFrame ret = new DataFrame();
Apache.Arrow.Schema arrowSchema = recordBatch.Schema;
int fieldIndex = 0;
IEnumerable<IArrowArray> arrowArrays = recordBatch.Arrays;
foreach (IArrowArray arrowArray in arrowArrays)
{
Field field = arrowSchema.GetFieldByIndex(fieldIndex);
AppendDataFrameColumnFromArrowArray(field, arrowArray, ret);
fieldIndex++;
}
return ret;
}

However, this implementation looks very suspicious.

RecordBatch is a disposable object. Apache Arrow by default uses NativeMemoryAllocator to allocate unmanaged memory (for example, this default allocator is used in Spark.Net to create RecorBatch and pass it to DataFrame.FromArrowRecordBatch factory method).
So it's up to a DataFrame to hold the link to the RecordBatch and correctly Dispose it. Or we have to copy the unmanaged readonly memory from the RecordBatch into managed buffers (that exactly what is happening in GetMutableBuffer on attempt to edit data), but in this case we can avoid using ReadOnlyBuffers at all.

@michaelgsharp
michaelgsharp merged commit 33342a2 into dotnet:mainMay 5, 2023
@janholo

janholo commented May 11, 2023

Copy link
Copy Markdown
ContributorAuthor

Hey @JakeRadMSFT, thanks for taking the time to review and merge the PR.

Sorry for not providing tests. What I still think is strange is that Apply and ApplyElementwise have similar signatures but do fundamentally different things. One changes "this" column and the other operates on an other column.

@ghostghost locked as resolved and limited conversation to collaborators Jun 28, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janholo@JakeRadMSFT@agocke@asmirnov82@michaelgsharp@jan-rhim
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix Apply in PrimitiveColumnContainer so it does not change source column - #6642

Merged
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main
May 5, 2023
Merged

Fix Apply in PrimitiveColumnContainer so it does not change source column#6642
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main

Conversation

@janholo

Copy link
Copy Markdown
Contributor

The Apply method calls SetValidityBit on the source container and not on the target container. That way the null check of the container values gets incorrect.

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

@janholo

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

@codecov

codecovBot commented May 2, 2023

Copy link
Copy Markdown

Codecov Report

Merging #6642 (53b90ae) into main (a18b9cb) will decrease coverage by 0.01%.
The diff coverage is 83.33%.

Additional details and impacted files
@@ Coverage Diff @@## main #6642 +/- ##
==========================================
- Coverage 68.53% 68.52% -0.01% 
==========================================
Files 1200 1200 Lines 250287 250279 -8 Branches 26093 26093 ==========================================
- Hits 171526 171509 -17 - Misses 71929 71936 +7 - Partials 6832 6834 +2 
FlagCoverage Δ
Debug68.52% <83.33%> (-0.01%)⬇️
production63.00% <83.33%> (-0.01%)⬇️
test88.83% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...icrosoft.Data.Analysis/PrimitiveColumnContainer.cs83.01% <83.33%> (-0.20%)⬇️

... and 2 files with indirect coverage changes

@JakeRadMSFT

JakeRadMSFT commented May 4, 2023

Copy link
Copy Markdown
Member

@janholo Thanks for your PR!

Could you add a test that fails before your change and passes after?

It would help me better understand the problem and it's solution.

Comment threadsrc/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs

@JakeRadMSFTJakeRadMSFT left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I dug into this a bunch to try and understand the differences and overlap. After doing that investigation - I like your change. It fixes 2 things.

What Does Apply Do?

The goal of Apply is to start with the value from this container ... call func with the value or default for the type ... then assign the result to resultContainer. Since it's possible the func changed a value from null to not null or vice-versa we have to update the validityBit.

Thing 1

This method used to get mutable versions of span and nullBitMap for the SourceContainer or this that we're not changing.

Your change makes it so we don't create mutable versions of source ... since it's not needed. Nothing should change with source.

Thing 2

It also fixes a bug where we were setting the validity bit on the SourceContainer because we were calling this.SetValidityBit instead of resultContainer.SetValidityBit.

DataFrameBuffer<byte> mutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(NullBitMapBuffers[b]);
NullBitMapBuffers[b] = mutableNullBitMapBuffer;
Span<byte> nullBitMapSpan = mutableNullBitMapBuffer.Span;
ReadOnlyDataFrameBuffer<T> sourceBuffer = Buffers[b];

@JakeRadMSFTJakeRadMSFTMay 4, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke@jaredpar

Just for my own learning purposes. Is this something that would be improved with the span changes in .NET? Would we no longer need to do all the GetMutableBuffer stuff?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically I'm wondering if there would be a better way to refactor out the code like this:

DataFrameBuffer<TResult> resultMutableBuffer = DataFrameBuffer<TResult>.GetMutableBuffer(resultBuffer);
resultContainer.Buffers[b] = resultMutableBuffer;
Span<TResult> resultSpan = resultMutableBuffer.Span;
DataFrameBuffer<byte> resultMutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(resultContainer.NullBitMapBuffers[b]);
resultContainer.NullBitMapBuffers[b] = resultMutableNullBitMapBuffer;
Span<byte> resultNullBitMapSpan = resultMutableNullBitMapBuffer.Span;

It's all over this file in some form.
Clone, Apply, ApplyElementWise, CloneAs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of very questionable stuff going on here. The code is going to great lengths to copy the original buffer to get a mutable version... and then passing the span to IsValid or calling the indexer, which both accept ReadOnlySpan.

And then there's the ReadOnlyDataFrameBuffer<T> type which takes a T : struct, but should be T : unmanaged.

This code could likely be simplified by extracting some helper methods, but I think the above issues indicate that someone should review the actual intent and ensure that the code reflects it.

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sort of what it felt like but I appreciate having some expert eyes on it! Thanks @agocke!

I might take this up as a pet project in the future :)

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke - I'm not sure it's 100% of the way there but I did a pass and opened a different PR.

https://github.com/dotnet/machinelearning/pull/6656/files

@asmirnov82asmirnov82May 29, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that PR already closed, but I have some comments regarding this GetMutableBuffer staff. It looks it's used for working with readonly memory, when column is created from the Apache Arrow RecordBatch in

public static DataFrame FromArrowRecordBatch(RecordBatch recordBatch)
{
DataFrame ret = new DataFrame();
Apache.Arrow.Schema arrowSchema = recordBatch.Schema;
int fieldIndex = 0;
IEnumerable<IArrowArray> arrowArrays = recordBatch.Arrays;
foreach (IArrowArray arrowArray in arrowArrays)
{
Field field = arrowSchema.GetFieldByIndex(fieldIndex);
AppendDataFrameColumnFromArrowArray(field, arrowArray, ret);
fieldIndex++;
}
return ret;
}

However, this implementation looks very suspicious.

RecordBatch is a disposable object. Apache Arrow by default uses NativeMemoryAllocator to allocate unmanaged memory (for example, this default allocator is used in Spark.Net to create RecorBatch and pass it to DataFrame.FromArrowRecordBatch factory method).
So it's up to a DataFrame to hold the link to the RecordBatch and correctly Dispose it. Or we have to copy the unmanaged readonly memory from the RecordBatch into managed buffers (that exactly what is happening in GetMutableBuffer on attempt to edit data), but in this case we can avoid using ReadOnlyBuffers at all.

@michaelgsharp
michaelgsharp merged commit 33342a2 into dotnet:mainMay 5, 2023
@janholo

janholo commented May 11, 2023

Copy link
Copy Markdown
ContributorAuthor

Hey @JakeRadMSFT, thanks for taking the time to review and merge the PR.

Sorry for not providing tests. What I still think is strange is that Apply and ApplyElementwise have similar signatures but do fundamentally different things. One changes "this" column and the other operates on an other column.

@ghostghost locked as resolved and limited conversation to collaborators Jun 28, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janholo@JakeRadMSFT@agocke@asmirnov82@michaelgsharp@jan-rhim
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix Apply in PrimitiveColumnContainer so it does not change source column - #6642

Merged
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main
May 5, 2023
Merged

Fix Apply in PrimitiveColumnContainer so it does not change source column#6642
michaelgsharp merged 1 commit into
dotnet:mainfrom
janholo:main

Conversation

@janholo

Copy link
Copy Markdown
Contributor

The Apply method calls SetValidityBit on the source container and not on the target container. That way the null check of the container values gets incorrect.

We are excited to review your PR.

So we can do the best job, please check:

  • There's a descriptive title that will make sense to other developers some time from now.
  • There's associated issues. All PR's should have issue(s) associated - unless a trivial self-evident change such as fixing a typo. You can use the format Fixes #nnnn in your description to cause GitHub to automatically close the issue(s) when your PR is merged.
  • Your change description explains what the change does, why you chose your approach, and anything else that reviewers should know.
  • You have included any necessary tests in the same PR.

@janholo

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

@codecov

codecovBot commented May 2, 2023

Copy link
Copy Markdown

Codecov Report

Merging #6642 (53b90ae) into main (a18b9cb) will decrease coverage by 0.01%.
The diff coverage is 83.33%.

Additional details and impacted files
@@ Coverage Diff @@## main #6642 +/- ##
==========================================
- Coverage 68.53% 68.52% -0.01% 
==========================================
Files 1200 1200 Lines 250287 250279 -8 Branches 26093 26093 ==========================================
- Hits 171526 171509 -17 - Misses 71929 71936 +7 - Partials 6832 6834 +2 
FlagCoverage Δ
Debug68.52% <83.33%> (-0.01%)⬇️
production63.00% <83.33%> (-0.01%)⬇️
test88.83% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted FilesCoverage Δ
...icrosoft.Data.Analysis/PrimitiveColumnContainer.cs83.01% <83.33%> (-0.20%)⬇️

... and 2 files with indirect coverage changes

@JakeRadMSFT

JakeRadMSFT commented May 4, 2023

Copy link
Copy Markdown
Member

@janholo Thanks for your PR!

Could you add a test that fails before your change and passes after?

It would help me better understand the problem and it's solution.

Comment threadsrc/Microsoft.Data.Analysis/PrimitiveColumnContainer.cs

@JakeRadMSFTJakeRadMSFT left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I dug into this a bunch to try and understand the differences and overlap. After doing that investigation - I like your change. It fixes 2 things.

What Does Apply Do?

The goal of Apply is to start with the value from this container ... call func with the value or default for the type ... then assign the result to resultContainer. Since it's possible the func changed a value from null to not null or vice-versa we have to update the validityBit.

Thing 1

This method used to get mutable versions of span and nullBitMap for the SourceContainer or this that we're not changing.

Your change makes it so we don't create mutable versions of source ... since it's not needed. Nothing should change with source.

Thing 2

It also fixes a bug where we were setting the validity bit on the SourceContainer because we were calling this.SetValidityBit instead of resultContainer.SetValidityBit.

DataFrameBuffer<byte> mutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(NullBitMapBuffers[b]);
NullBitMapBuffers[b] = mutableNullBitMapBuffer;
Span<byte> nullBitMapSpan = mutableNullBitMapBuffer.Span;
ReadOnlyDataFrameBuffer<T> sourceBuffer = Buffers[b];

@JakeRadMSFTJakeRadMSFTMay 4, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke@jaredpar

Just for my own learning purposes. Is this something that would be improved with the span changes in .NET? Would we no longer need to do all the GetMutableBuffer stuff?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically I'm wondering if there would be a better way to refactor out the code like this:

DataFrameBuffer<TResult> resultMutableBuffer = DataFrameBuffer<TResult>.GetMutableBuffer(resultBuffer);
resultContainer.Buffers[b] = resultMutableBuffer;
Span<TResult> resultSpan = resultMutableBuffer.Span;
DataFrameBuffer<byte> resultMutableNullBitMapBuffer = DataFrameBuffer<byte>.GetMutableBuffer(resultContainer.NullBitMapBuffers[b]);
resultContainer.NullBitMapBuffers[b] = resultMutableNullBitMapBuffer;
Span<byte> resultNullBitMapSpan = resultMutableNullBitMapBuffer.Span;

It's all over this file in some form.
Clone, Apply, ApplyElementWise, CloneAs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of very questionable stuff going on here. The code is going to great lengths to copy the original buffer to get a mutable version... and then passing the span to IsValid or calling the indexer, which both accept ReadOnlySpan.

And then there's the ReadOnlyDataFrameBuffer<T> type which takes a T : struct, but should be T : unmanaged.

This code could likely be simplified by extracting some helper methods, but I think the above issues indicate that someone should review the actual intent and ensure that the code reflects it.

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's sort of what it felt like but I appreciate having some expert eyes on it! Thanks @agocke!

I might take this up as a pet project in the future :)

@JakeRadMSFTJakeRadMSFTMay 5, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agocke - I'm not sure it's 100% of the way there but I did a pass and opened a different PR.

https://github.com/dotnet/machinelearning/pull/6656/files

@asmirnov82asmirnov82May 29, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that PR already closed, but I have some comments regarding this GetMutableBuffer staff. It looks it's used for working with readonly memory, when column is created from the Apache Arrow RecordBatch in

public static DataFrame FromArrowRecordBatch(RecordBatch recordBatch)
{
DataFrame ret = new DataFrame();
Apache.Arrow.Schema arrowSchema = recordBatch.Schema;
int fieldIndex = 0;
IEnumerable<IArrowArray> arrowArrays = recordBatch.Arrays;
foreach (IArrowArray arrowArray in arrowArrays)
{
Field field = arrowSchema.GetFieldByIndex(fieldIndex);
AppendDataFrameColumnFromArrowArray(field, arrowArray, ret);
fieldIndex++;
}
return ret;
}

However, this implementation looks very suspicious.

RecordBatch is a disposable object. Apache Arrow by default uses NativeMemoryAllocator to allocate unmanaged memory (for example, this default allocator is used in Spark.Net to create RecorBatch and pass it to DataFrame.FromArrowRecordBatch factory method).
So it's up to a DataFrame to hold the link to the RecordBatch and correctly Dispose it. Or we have to copy the unmanaged readonly memory from the RecordBatch into managed buffers (that exactly what is happening in GetMutableBuffer on attempt to edit data), but in this case we can avoid using ReadOnlyBuffers at all.

@michaelgsharp
michaelgsharp merged commit 33342a2 into dotnet:mainMay 5, 2023
@janholo

janholo commented May 11, 2023

Copy link
Copy Markdown
ContributorAuthor

Hey @JakeRadMSFT, thanks for taking the time to review and merge the PR.

Sorry for not providing tests. What I still think is strange is that Apply and ApplyElementwise have similar signatures but do fundamentally different things. One changes "this" column and the other operates on an other column.

@ghostghost locked as resolved and limited conversation to collaborators Jun 28, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janholo@JakeRadMSFT@agocke@asmirnov82@michaelgsharp@jan-rhim