Repository: scikit-hep/awkward

Issue:
Using a behavior override on whole object touches whole object
### Version of Awkward Array

2.1.2

### Description and code to reproduce

The following reproducer should only need to touch ```'jet_list_outer!'``` and ```jet_pt!```:
```python3
import json
import awkward as ak
import numpy as np

fromjson = {
    "class": "RecordArray",
    "fields": ["muon", "jet"],
    "contents": [
        {
            "class": "ListOffsetArray",
            "offsets": "i64",
            "content": {
                "class": "RecordArray",
                "fields": ["pt", "eta", "phi", "crossref"],
                "contents": [
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "muon_pt!",
                    },
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "muon_eta!",
                    },
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "muon_phi!",
                    },
                    {
                        "class": "ListOffsetArray",
                        "offsets": "i64",
                        "content": {
                            "class": "NumpyArray",
                            "primitive": "int64",
                            "inner_shape": [],
                            "parameters": {},
                            "form_key": "muon_crossref_content!",
                        },
                        "parameters": {},
                        "form_key": "muon_crossref_index!",
                    },
                ],
                "parameters": {},
                "form_key": "muon_record!",
            },
            "parameters": {},
            "form_key": "muon_list_outer!",
        },
        {
            "class": "ListOffsetArray",
            "offsets": "i64",
            "content": {
                "class": "RecordArray",
                "fields": ["pt", "eta", "phi", "crossref", "thing1"],
                "contents": [
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "jet_pt!",
                    },
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "jet_eta!",
                    },
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "jet_phi!",
                    },
                    {
                        "class": "ListOffsetArray",
                        "offsets": "i64",
                        "content": {
                            "class": "NumpyArray",
                            "primitive": "int64",
                            "inner_shape": [],
                            "parameters": {},
                            "form_key": "jet_crossref_content!",
                        },
                        "parameters": {},
                        "form_key": "jet_crossref_index!",
                    },
                    {
                        "class": "NumpyArray",
                        "primitive": "int64",
                        "inner_shape": [],
                        "parameters": {},
                        "form_key": "jet_thing1!",
                    },
                ],
                "parameters": {"__record__": "TestBehavior"},
                "form_key": "jet_record!",
            },
            "parameters": {},
            "form_key": "jet_list_outer!",
        },
    ],
    "parameters": {},
    "form_key": "outer!",
}


@ak.mixin_class(ak.behavior)
class TestBehavior:
    @ak.mixin_class_method(np.add, {"TestBehavior"})
    def add(self, other):
        return ak.zip(
            {
                "pt": self.pt + other.pt,
            },
            with_name="TestBehavior",
            behavior=self.behavior,
        )


form = ak.forms.from_json(json.dumps(fromjson))

ttlayout, report = ak._nplikes.typetracer.typetracer_with_report(form)

ttarray = ak.Array(ttlayout, behavior=ak.behavior)

a = ak.combinations(ttarray.jet, 3, fields=["j1", "j2", "j3"])

print("combo: ", report.data_touched)

a["something"] = a.j1 + a.j2 + a.j3

print("assign:", report.data_touched)
```
However, its outcome is:
```
combo:  ['jet_list_outer!']
assign: ['jet_list_outer!', 'jet_pt!', 'jet_eta!', 'jet_phi!', 'jet_crossref_index!', 'jet_thing1!']
```

Which is everything in this mock jet object. This means that, as things are, using behaviors in dask awkward becomes data-inefficient very quickly.

@agoose77 @alexander-held 


Discussion:
The assignment broadcasts `(a.j1 + a.j2 + a.j3)` with `a`. I'm not 100% sure whether broadcasting an array of records (possibly nested in lists) uses the `IndexedArray` lazy-carry or not.

Resolving patch:
diff --git a/src/awkward/_nplikes/typetracer.py b/src/awkward/_nplikes/typetracer.py
--- a/src/awkward/_nplikes/typetracer.py
+++ b/src/awkward/_nplikes/typetracer.py
@@ -607,8 +607,6 @@ def asarray(
         dtype: numpy.dtype | None = None,
         copy: bool | None = None,
     ) -> TypeTracerArray:
-        try_touch_data(obj)
-
         if isinstance(obj, ak.index.Index):
             obj = obj.data
 
@@ -618,11 +616,16 @@ def asarray(
 
             if dtype is None:
                 return obj
-            elif copy is False and dtype != obj.dtype:
+            elif dtype == obj.dtype:
+                return TypeTracerArray._new(
+                    dtype, obj.shape, form_key=form_key, report=report
+                )
+            elif copy is False:
                 raise ValueError(
                     "asarray was called with copy=False for an array of a different dtype"
                 )
             else:
+                try_touch_data(obj)
                 return TypeTracerArray._new(
                     dtype, obj.shape, form_key=form_key, report=report
                 )


Test patch:
diff --git a/tests/test_2395_copy_asarray_touch.py b/tests/test_2395_copy_asarray_touch.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2395_copy_asarray_touch.py
@@ -0,0 +1,33 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+import numpy as np
+import pytest  # noqa: F401
+
+import awkward as ak
+from awkward._nplikes.typetracer import TypeTracer, typetracer_with_report
+
+typetracer = TypeTracer.instance()
+
+
+def test_no_copy():
+    form = ak.forms.NumpyForm("int64", form_key="buffer")
+    layout, report = typetracer_with_report(form)
+
+    typetracer.asarray(layout.data)
+    assert not report.data_touched
+
+
+def test_no_copy_dtype():
+    form = ak.forms.NumpyForm("int64", form_key="buffer")
+    layout, report = typetracer_with_report(form)
+
+    typetracer.asarray(layout.data, dtype=np.int64)
+    assert not report.data_touched
+
+
+def test_copy_touch():
+    form = ak.forms.NumpyForm("int64", form_key="buffer")
+    layout, report = typetracer_with_report(form)
+
+    typetracer.asarray(layout.data, dtype=np.int32)
+    assert report.data_touched == ["buffer"]